Merge branch 'develop' into enhancement/poetry-invoke

This commit is contained in:
Blake Harnden 2020-07-07 19:44:41 -07:00
commit 3949bd6d1b
201 changed files with 8902 additions and 8679 deletions

View file

@ -18,7 +18,7 @@ jobs:
cd daemon cd daemon
cp setup.py.in setup.py cp setup.py.in setup.py
cp core/constants.py.in core/constants.py cp core/constants.py.in core/constants.py
sed -i 's/True/False/g' core/constants.py sed -i 's/required=True/required=False/g' core/emulator/coreemu.py
pipenv sync --dev pipenv sync --dev
- name: isort - name: isort
run: | run: |

View file

@ -1,3 +1,40 @@
## 2020-06-11 CORE 6.5.0
* Breaking Changes
* CoreNode.newnetif - both parameters are required and now takes an InterfaceData object as its second parameter
* CoreNetworkBase.linkconfig - now takes a LinkOptions parameter instead of a subset of some of the options (ie bandwidth, delay, etc)
* \#453 - Session.add_node and Session.get_node now requires the node class you expect to create/retrieve
* \#458 - rj45 cleanup to only inherit from one class
* Enhancements
* fixed issues with handling bad commands for TLV execute messages
* removed unused boot.sh from CoreNode types
* added linkconfig to CoreNetworkBase and cleaned up function signature
* emane position hook now saves geo position to node
* emane pathloss support
* core.emulator.emudata leveraged dataclass and type hinting
* \#459 - updated transport type usage to an enum
* \#460 - updated network policy type usage to an enum
* Python GUI Enhancements
* fixed throughput events do not work for joined sessions
* fixed exiting app with a toolbar picker showing
* fixed issue with creating interfaces and reusing subnets after deletion
* fixed issue with moving text shapes
* fixed scaling with custom node selected
* fixed toolbar state switching issues
* enable/disable toolbar when running stop/start
* marker config integrated into toolbar
* improved color picker layout
* shapes can now be moved while drawing shapes
* added observers to toolbar in run mode
* gRPC API
* node events will now have geo positional data
* node geo data is now returned in get_session and get_node calls
* \#451 - added wlan link api to allow direct linking/unlinking of wireless links between nodes
* \#462 - added streaming call for sending node position/geo changes
* \#463 - added streaming call for emane pathloss events
* Bugfixes
* \#454 - fixed issue creating docker nodes, but containers are now required to have networking tools
* \#466 - fixed issue in python gui when xml file is loading nodes with no ip4 addresses
## 2020-05-11 CORE 6.4.0 ## 2020-05-11 CORE 6.4.0
* Enhancements * Enhancements
* updates to core-route-monitor, allow specific session, configurable settings, and properly * updates to core-route-monitor, allow specific session, configurable settings, and properly

View file

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

View file

@ -1,16 +1,14 @@
""" """
gRpc client for interfacing with CORE, when gRPC mode is enabled. gRpc client for interfacing with CORE.
""" """
import logging import logging
import threading import threading
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Callable, Dict, Generator, List from typing import Any, Callable, Dict, Generator, Iterable, List, Optional
import grpc import grpc
import netaddr
from core import utils
from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc
from core.api.grpc.configservices_pb2 import ( from core.api.grpc.configservices_pb2 import (
GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsRequest,
@ -31,6 +29,8 @@ from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
EmaneLinkResponse, EmaneLinkResponse,
EmaneModelConfig, EmaneModelConfig,
EmanePathlossesRequest,
EmanePathlossesResponse,
GetEmaneConfigRequest, GetEmaneConfigRequest,
GetEmaneConfigResponse, GetEmaneConfigResponse,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
@ -92,6 +92,7 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkRequest, WlanLinkRequest,
WlanLinkResponse, WlanLinkResponse,
) )
from core.emulator.data import IpPrefixes
class InterfaceHelper: class InterfaceHelper:
@ -107,78 +108,29 @@ class InterfaceHelper:
:param ip6_prefix: ip6 prefix to use for generation :param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided :raises ValueError: when both ip4 and ip6 prefixes have not been provided
""" """
if not ip4_prefix and not ip6_prefix: self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix)
raise ValueError("ip4 or ip6 must be provided")
self.ip4 = None def create_iface(
if ip4_prefix: self, node_id: int, iface_id: int, name: str = None, mac: str = None
self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None
if ip6_prefix:
self.ip6 = netaddr.IPNetwork(ip6_prefix)
def ip4_address(self, node_id: int) -> str:
"""
Convenience method to return the IP4 address for a node.
:param node_id: node id to get IP4 address for
:return: IP4 address or None
"""
if not self.ip4:
raise ValueError("ip4 prefixes have not been set")
return str(self.ip4[node_id])
def ip6_address(self, node_id: int) -> str:
"""
Convenience method to return the IP6 address for a node.
:param node_id: node id to get IP6 address for
:return: IP4 address or None
"""
if not self.ip6:
raise ValueError("ip6 prefixes have not been set")
return str(self.ip6[node_id])
def create_interface(
self, node_id: int, interface_id: int, name: str = None, mac: str = None
) -> core_pb2.Interface: ) -> core_pb2.Interface:
""" """
Creates interface data for linking nodes, using the nodes unique id for Create an interface protobuf object.
generation, along with a random mac address, unless provided.
:param node_id: node id to create interface for :param node_id: node id to create interface for
:param interface_id: interface id for interface :param iface_id: interface id
:param name: name to set for interface, default is eth{id} :param name: name of interface
:param mac: mac address to use for this interface, default is random :param mac: mac address for interface
generation :return: interface protobuf
:return: new interface data for the provided node
""" """
# generate ip4 data iface_data = self.prefixes.gen_iface(node_id, name, mac)
ip4 = None
ip4_mask = None
if self.ip4:
ip4 = self.ip4_address(node_id)
ip4_mask = self.ip4.prefixlen
# generate ip6 data
ip6 = None
ip6_mask = None
if self.ip6:
ip6 = self.ip6_address(node_id)
ip6_mask = self.ip6.prefixlen
# random mac
if not mac:
mac = utils.random_mac()
return core_pb2.Interface( return core_pb2.Interface(
id=interface_id, id=iface_id,
name=name, name=iface_data.name,
ip4=ip4, ip4=iface_data.ip4,
ip4mask=ip4_mask, ip4_mask=iface_data.ip4_mask,
ip6=ip6, ip6=iface_data.ip6,
ip6mask=ip6_mask, ip6_mask=iface_data.ip6_mask,
mac=str(mac), mac=iface_data.mac,
) )
@ -225,10 +177,10 @@ class CoreGrpcClient:
:param address: grpc server address to connect to :param address: grpc server address to connect to
""" """
self.address = address self.address: str = address
self.stub = None self.stub: Optional[core_pb2_grpc.CoreApiStub] = None
self.channel = None self.channel: Optional[grpc.Channel] = None
self.proxy = proxy self.proxy: bool = proxy
def start_session( def start_session(
self, self,
@ -287,6 +239,7 @@ class CoreGrpcClient:
:param session_id: id of session :param session_id: id of session
:return: stop session response :return: stop session response
:raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.StopSessionRequest(session_id=session_id) request = core_pb2.StopSessionRequest(session_id=session_id)
return self.stub.StopSession(request) return self.stub.StopSession(request)
@ -483,7 +436,7 @@ class CoreGrpcClient:
session_id: int, session_id: int,
handler: Callable[[core_pb2.Event], None], handler: Callable[[core_pb2.Event], None],
events: List[core_pb2.Event] = None, events: List[core_pb2.Event] = None,
) -> Any: ) -> grpc.Channel:
""" """
Listen for session events. Listen for session events.
@ -500,7 +453,7 @@ class CoreGrpcClient:
def throughputs( def throughputs(
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
) -> Any: ) -> grpc.Channel:
""" """
Listen for throughput events with information for interfaces and bridges. Listen for throughput events with information for interfaces and bridges.
@ -515,17 +468,20 @@ class CoreGrpcClient:
return stream return stream
def add_node( def add_node(
self, session_id: int, node: core_pb2.Node self, session_id: int, node: core_pb2.Node, source: str = None
) -> core_pb2.AddNodeResponse: ) -> core_pb2.AddNodeResponse:
""" """
Add node to session. Add node to session.
:param session_id: session id :param session_id: session id
:param node: node to add :param node: node to add
:param source: source application
:return: response with node id :return: response with node id
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.AddNodeRequest(session_id=session_id, node=node) request = core_pb2.AddNodeRequest(
session_id=session_id, node=node, source=source
)
return self.stub.AddNode(request) return self.stub.AddNode(request)
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse: def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
@ -546,8 +502,8 @@ class CoreGrpcClient:
node_id: int, node_id: int,
position: core_pb2.Position = None, position: core_pb2.Position = None,
icon: str = None, icon: str = None,
source: str = None,
geo: core_pb2.Geo = None, geo: core_pb2.Geo = None,
source: str = None,
) -> core_pb2.EditNodeResponse: ) -> core_pb2.EditNodeResponse:
""" """
Edit a node, currently only changes position. Edit a node, currently only changes position.
@ -556,8 +512,8 @@ class CoreGrpcClient:
:param node_id: node id :param node_id: node id
:param position: position to set node to :param position: position to set node to
:param icon: path to icon for gui to use for node :param icon: path to icon for gui to use for node
:param source: application source editing node
:param geo: lon,lat,alt location for node :param geo: lon,lat,alt location for node
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or node doesn't exist :raises grpc.RpcError: when session or node doesn't exist
""" """
@ -571,20 +527,42 @@ class CoreGrpcClient:
) )
return self.stub.EditNode(request) return self.stub.EditNode(request)
def delete_node(self, session_id: int, node_id: int) -> core_pb2.DeleteNodeResponse: def move_nodes(
self, move_iterator: Iterable[core_pb2.MoveNodesRequest]
) -> core_pb2.MoveNodesResponse:
"""
Stream node movements using the provided iterator.
:param move_iterator: iterator for generating node movements
:return: move nodes response
:raises grpc.RpcError: when session or nodes do not exist
"""
return self.stub.MoveNodes(move_iterator)
def delete_node(
self, session_id: int, node_id: int, source: str = None
) -> core_pb2.DeleteNodeResponse:
""" """
Delete node from session. Delete node from session.
:param session_id: session id :param session_id: session id
:param node_id: node id :param node_id: node id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.DeleteNodeRequest(session_id=session_id, node_id=node_id) request = core_pb2.DeleteNodeRequest(
session_id=session_id, node_id=node_id, source=source
)
return self.stub.DeleteNode(request) return self.stub.DeleteNode(request)
def node_command( def node_command(
self, session_id: int, node_id: int, command: str self,
session_id: int,
node_id: int,
command: str,
wait: bool = True,
shell: bool = False,
) -> core_pb2.NodeCommandResponse: ) -> core_pb2.NodeCommandResponse:
""" """
Send command to a node and get the output. Send command to a node and get the output.
@ -592,11 +570,17 @@ class CoreGrpcClient:
:param session_id: session id :param session_id: session id
:param node_id: node id :param node_id: node id
:param command: command to run on node :param command: command to run on node
:param wait: wait for command to complete
:param shell: send shell command
:return: response with command combined stdout/stderr :return: response with command combined stdout/stderr
:raises grpc.RpcError: when session or node doesn't exist :raises grpc.RpcError: when session or node doesn't exist
""" """
request = core_pb2.NodeCommandRequest( request = core_pb2.NodeCommandRequest(
session_id=session_id, node_id=node_id, command=command session_id=session_id,
node_id=node_id,
command=command,
wait=wait,
shell=shell,
) )
return self.stub.NodeCommand(request) return self.stub.NodeCommand(request)
@ -633,91 +617,101 @@ class CoreGrpcClient:
def add_link( def add_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
interface_one: core_pb2.Interface = None, iface1: core_pb2.Interface = None,
interface_two: core_pb2.Interface = None, iface2: core_pb2.Interface = None,
options: core_pb2.LinkOptions = None, options: core_pb2.LinkOptions = None,
source: str = None,
) -> core_pb2.AddLinkResponse: ) -> core_pb2.AddLinkResponse:
""" """
Add a link between nodes. Add a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param interface_one: node one interface data :param iface1: node one interface data
:param interface_two: node two interface data :param iface2: node two interface data
:param options: options for link (jitter, bandwidth, etc) :param options: options for link (jitter, bandwidth, etc)
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist :raises grpc.RpcError: when session or one of the nodes don't exist
""" """
link = core_pb2.Link( link = core_pb2.Link(
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
type=core_pb2.LinkType.WIRED, type=core_pb2.LinkType.WIRED,
interface_one=interface_one, iface1=iface1,
interface_two=interface_two, iface2=iface2,
options=options, options=options,
) )
request = core_pb2.AddLinkRequest(session_id=session_id, link=link) request = core_pb2.AddLinkRequest(
session_id=session_id, link=link, source=source
)
return self.stub.AddLink(request) return self.stub.AddLink(request)
def edit_link( def edit_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
options: core_pb2.LinkOptions, options: core_pb2.LinkOptions,
interface_one_id: int = None, iface1_id: int = None,
interface_two_id: int = None, iface2_id: int = None,
source: str = None,
) -> core_pb2.EditLinkResponse: ) -> core_pb2.EditLinkResponse:
""" """
Edit a link between nodes. Edit a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param options: options for link (jitter, bandwidth, etc) :param options: options for link (jitter, bandwidth, etc)
:param interface_one_id: node one interface id :param iface1_id: node one interface id
:param interface_two_id: node two interface id :param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist :raises grpc.RpcError: when session or one of the nodes don't exist
""" """
request = core_pb2.EditLinkRequest( request = core_pb2.EditLinkRequest(
session_id=session_id, session_id=session_id,
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
options=options, options=options,
interface_one_id=interface_one_id, iface1_id=iface1_id,
interface_two_id=interface_two_id, iface2_id=iface2_id,
source=source,
) )
return self.stub.EditLink(request) return self.stub.EditLink(request)
def delete_link( def delete_link(
self, self,
session_id: int, session_id: int,
node_one_id: int, node1_id: int,
node_two_id: int, node2_id: int,
interface_one_id: int = None, iface1_id: int = None,
interface_two_id: int = None, iface2_id: int = None,
source: str = None,
) -> core_pb2.DeleteLinkResponse: ) -> core_pb2.DeleteLinkResponse:
""" """
Delete a link between nodes. Delete a link between nodes.
:param session_id: session id :param session_id: session id
:param node_one_id: node one id :param node1_id: node one id
:param node_two_id: node two id :param node2_id: node two id
:param interface_one_id: node one interface id :param iface1_id: node one interface id
:param interface_two_id: node two interface id :param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.DeleteLinkRequest( request = core_pb2.DeleteLinkRequest(
session_id=session_id, session_id=session_id,
node_one_id=node_one_id, node1_id=node1_id,
node_two_id=node_two_id, node2_id=node2_id,
interface_one_id=interface_one_id, iface1_id=iface1_id,
interface_two_id=interface_two_id, iface2_id=iface2_id,
source=source,
) )
return self.stub.DeleteLink(request) return self.stub.DeleteLink(request)
@ -1052,7 +1046,7 @@ class CoreGrpcClient:
return self.stub.GetEmaneModels(request) return self.stub.GetEmaneModels(request)
def get_emane_model_config( def get_emane_model_config(
self, session_id: int, node_id: int, model: str, interface_id: int = -1 self, session_id: int, node_id: int, model: str, iface_id: int = -1
) -> GetEmaneModelConfigResponse: ) -> GetEmaneModelConfigResponse:
""" """
Get emane model configuration for a node or a node's interface. Get emane model configuration for a node or a node's interface.
@ -1060,12 +1054,12 @@ class CoreGrpcClient:
:param session_id: session id :param session_id: session id
:param node_id: node id :param node_id: node id
:param model: emane model name :param model: emane model name
:param interface_id: node interface id :param iface_id: node interface id
:return: response with a list of configuration groups :return: response with a list of configuration groups
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
request = GetEmaneModelConfigRequest( request = GetEmaneModelConfigRequest(
session_id=session_id, node_id=node_id, model=model, interface=interface_id session_id=session_id, node_id=node_id, model=model, iface_id=iface_id
) )
return self.stub.GetEmaneModelConfig(request) return self.stub.GetEmaneModelConfig(request)
@ -1074,8 +1068,8 @@ class CoreGrpcClient:
session_id: int, session_id: int,
node_id: int, node_id: int,
model: str, model: str,
config: Dict[str, str], config: Dict[str, str] = None,
interface_id: int = -1, iface_id: int = -1,
) -> SetEmaneModelConfigResponse: ) -> SetEmaneModelConfigResponse:
""" """
Set emane model configuration for a node or a node's interface. Set emane model configuration for a node or a node's interface.
@ -1084,12 +1078,12 @@ class CoreGrpcClient:
:param node_id: node id :param node_id: node id
:param model: emane model name :param model: emane model name
:param config: emane model configuration :param config: emane model configuration
:param interface_id: node interface id :param iface_id: node interface id
:return: response with result of success or failure :return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
model_config = EmaneModelConfig( model_config = EmaneModelConfig(
node_id=node_id, model=model, config=config, interface_id=interface_id node_id=node_id, model=model, config=config, iface_id=iface_id
) )
request = SetEmaneModelConfigRequest( request = SetEmaneModelConfigRequest(
session_id=session_id, emane_model_config=model_config session_id=session_id, emane_model_config=model_config
@ -1098,9 +1092,9 @@ class CoreGrpcClient:
def get_emane_model_configs(self, session_id: int) -> GetEmaneModelConfigsResponse: def get_emane_model_configs(self, session_id: int) -> GetEmaneModelConfigsResponse:
""" """
Get all emane model configurations for a session. Get all EMANE model configurations for a session.
:param session_id: session id :param session_id: session to get emane model configs
:return: response with a dictionary of node/interface ids to configurations :return: response with a dictionary of node/interface ids to configurations
:raises grpc.RpcError: when session doesn't exist :raises grpc.RpcError: when session doesn't exist
""" """
@ -1111,9 +1105,10 @@ class CoreGrpcClient:
""" """
Save the current scenario to an XML file. Save the current scenario to an XML file.
:param session_id: session id :param session_id: session to save xml file for
:param file_path: local path to save scenario XML file to :param file_path: local path to save scenario XML file to
:return: nothing :return: nothing
:raises grpc.RpcError: when session doesn't exist
""" """
request = core_pb2.SaveXmlRequest(session_id=session_id) request = core_pb2.SaveXmlRequest(session_id=session_id)
response = self.stub.SaveXml(request) response = self.stub.SaveXml(request)
@ -1134,51 +1129,79 @@ class CoreGrpcClient:
return self.stub.OpenXml(request) return self.stub.OpenXml(request)
def emane_link( def emane_link(
self, session_id: int, nem_one: int, nem_two: int, linked: bool self, session_id: int, nem1: int, nem2: int, linked: bool
) -> EmaneLinkResponse: ) -> EmaneLinkResponse:
""" """
Helps broadcast wireless link/unlink between EMANE nodes. Helps broadcast wireless link/unlink between EMANE nodes.
:param session_id: session id :param session_id: session to emane link
:param nem_one: :param nem1: first nem for emane link
:param nem_two: :param nem2: second nem for emane link
:param linked: True to link, False to unlink :param linked: True to link, False to unlink
:return: core_pb2.EmaneLinkResponse :return: get emane link response
:raises grpc.RpcError: when session or nodes related to nems do not exist
""" """
request = EmaneLinkRequest( request = EmaneLinkRequest(
session_id=session_id, nem_one=nem_one, nem_two=nem_two, linked=linked session_id=session_id, nem1=nem1, nem2=nem2, linked=linked
) )
return self.stub.EmaneLink(request) return self.stub.EmaneLink(request)
def get_interfaces(self) -> core_pb2.GetInterfacesResponse: def get_ifaces(self) -> core_pb2.GetInterfacesResponse:
""" """
Retrieves a list of interfaces available on the host machine that are not Retrieves a list of interfaces available on the host machine that are not
a part of a CORE session. a part of a CORE session.
:return: core_pb2.GetInterfacesResponse :return: get interfaces response
""" """
request = core_pb2.GetInterfacesRequest() request = core_pb2.GetInterfacesRequest()
return self.stub.GetInterfaces(request) return self.stub.GetInterfaces(request)
def get_config_services(self) -> GetConfigServicesResponse: def get_config_services(self) -> GetConfigServicesResponse:
"""
Retrieve all known config services.
:return: get config services response
"""
request = GetConfigServicesRequest() request = GetConfigServicesRequest()
return self.stub.GetConfigServices(request) return self.stub.GetConfigServices(request)
def get_config_service_defaults( def get_config_service_defaults(
self, name: str self, name: str
) -> GetConfigServiceDefaultsResponse: ) -> GetConfigServiceDefaultsResponse:
"""
Retrieves config service default values.
:param name: name of service to get defaults for
:return: get config service defaults
"""
request = GetConfigServiceDefaultsRequest(name=name) request = GetConfigServiceDefaultsRequest(name=name)
return self.stub.GetConfigServiceDefaults(request) return self.stub.GetConfigServiceDefaults(request)
def get_node_config_service_configs( def get_node_config_service_configs(
self, session_id: int self, session_id: int
) -> GetNodeConfigServiceConfigsResponse: ) -> GetNodeConfigServiceConfigsResponse:
"""
Retrieves all node config service configurations for a session.
:param session_id: session to get config service configurations for
:return: get node config service configs response
:raises grpc.RpcError: when session doesn't exist
"""
request = GetNodeConfigServiceConfigsRequest(session_id=session_id) request = GetNodeConfigServiceConfigsRequest(session_id=session_id)
return self.stub.GetNodeConfigServiceConfigs(request) return self.stub.GetNodeConfigServiceConfigs(request)
def get_node_config_service( def get_node_config_service(
self, session_id: int, node_id: int, name: str self, session_id: int, node_id: int, name: str
) -> GetNodeConfigServiceResponse: ) -> GetNodeConfigServiceResponse:
"""
Retrieves information for a specific config service on a node.
:param session_id: session node belongs to
:param node_id: id of node to get service information from
:param name: name of service
:return: get node config service response
:raises grpc.RpcError: when session or node doesn't exist
"""
request = GetNodeConfigServiceRequest( request = GetNodeConfigServiceRequest(
session_id=session_id, node_id=node_id, name=name session_id=session_id, node_id=node_id, name=name
) )
@ -1187,37 +1210,92 @@ class CoreGrpcClient:
def get_node_config_services( def get_node_config_services(
self, session_id: int, node_id: int self, session_id: int, node_id: int
) -> GetNodeConfigServicesResponse: ) -> GetNodeConfigServicesResponse:
"""
Retrieves the config services currently assigned to a node.
:param session_id: session node belongs to
:param node_id: id of node to get config services for
:return: get node config services response
:raises grpc.RpcError: when session or node doesn't exist
"""
request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id) request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id)
return self.stub.GetNodeConfigServices(request) return self.stub.GetNodeConfigServices(request)
def set_node_config_service( def set_node_config_service(
self, session_id: int, node_id: int, name: str, config: Dict[str, str] self, session_id: int, node_id: int, name: str, config: Dict[str, str]
) -> SetNodeConfigServiceResponse: ) -> SetNodeConfigServiceResponse:
"""
Assigns a config service to a node with the provided configuration.
:param session_id: session node belongs to
:param node_id: id of node to assign config service to
:param name: name of service
:param config: service configuration
:return: set node config service response
:raises grpc.RpcError: when session or node doesn't exist
"""
request = SetNodeConfigServiceRequest( request = SetNodeConfigServiceRequest(
session_id=session_id, node_id=node_id, name=name, config=config session_id=session_id, node_id=node_id, name=name, config=config
) )
return self.stub.SetNodeConfigService(request) return self.stub.SetNodeConfigService(request)
def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelResponse: def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelResponse:
"""
Retrieves the current emane event channel being used for a session.
:param session_id: session to get emane event channel for
:return: emane event channel response
:raises grpc.RpcError: when session doesn't exist
"""
request = GetEmaneEventChannelRequest(session_id=session_id) request = GetEmaneEventChannelRequest(session_id=session_id)
return self.stub.GetEmaneEventChannel(request) return self.stub.GetEmaneEventChannel(request)
def execute_script(self, script: str) -> ExecuteScriptResponse: def execute_script(self, script: str) -> ExecuteScriptResponse:
"""
Executes a python script given context of the current CoreEmu object.
:param script: script to execute
:return: execute script response
"""
request = ExecuteScriptRequest(script=script) request = ExecuteScriptRequest(script=script)
return self.stub.ExecuteScript(request) return self.stub.ExecuteScript(request)
def wlan_link( def wlan_link(
self, session_id: int, wlan: int, node_one: int, node_two: int, linked: bool self, session_id: int, wlan_id: int, node1_id: int, node2_id: int, linked: bool
) -> WlanLinkResponse: ) -> WlanLinkResponse:
"""
Links/unlinks nodes on the same WLAN.
:param session_id: session id containing wlan and nodes
:param wlan_id: wlan nodes must belong to
:param node1_id: first node of pair to link/unlink
:param node2_id: second node of pair to link/unlin
:param linked: True to link, False to unlink
:return: wlan link response
:raises grpc.RpcError: when session or one of the nodes do not exist
"""
request = WlanLinkRequest( request = WlanLinkRequest(
session_id=session_id, session_id=session_id,
wlan=wlan, wlan=wlan_id,
node_one=node_one, node1_id=node1_id,
node_two=node_two, node2_id=node2_id,
linked=linked, linked=linked,
) )
return self.stub.WlanLink(request) return self.stub.WlanLink(request)
def emane_pathlosses(
self, pathloss_iterator: Iterable[EmanePathlossesRequest]
) -> EmanePathlossesResponse:
"""
Stream EMANE pathloss events.
:param pathloss_iterator: iterator for sending emane pathloss events
:return: emane pathloss response
:raises grpc.RpcError: when a pathloss event session or one of the nodes do not
exist
"""
return self.stub.EmanePathlosses(pathloss_iterator)
def connect(self) -> None: def connect(self) -> None:
""" """
Open connection to server, must be closed manually. Open connection to server, must be closed manually.

View file

@ -1,6 +1,6 @@
import logging import logging
from queue import Empty, Queue from queue import Empty, Queue
from typing import Iterable from typing import Iterable, Optional
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.grpcutils import convert_link from core.api.grpc.grpcutils import convert_link
@ -15,115 +15,127 @@ from core.emulator.data import (
from core.emulator.session import Session from core.emulator.session import Session
def handle_node_event(event: NodeData) -> core_pb2.NodeEvent: def handle_node_event(node_data: NodeData) -> core_pb2.Event:
""" """
Handle node event when there is a node event Handle node event when there is a node event
:param event: node data :param node_data: node data
:return: node event that contains node id, name, model, position, and services :return: node event that contains node id, name, model, position, and services
""" """
position = core_pb2.Position(x=event.x_position, y=event.y_position) node = node_data.node
geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude) x, y, _ = node.position.get()
position = core_pb2.Position(x=x, y=y)
lon, lat, alt = node.position.get_geo()
geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
services = [x.name for x in node.services]
node_proto = core_pb2.Node( node_proto = core_pb2.Node(
id=event.id, id=node.id,
name=event.name, name=node.name,
model=event.model, model=node.type,
position=position, position=position,
geo=geo, geo=geo,
services=event.services, services=services,
) )
return core_pb2.NodeEvent(node=node_proto, source=event.source) message_type = node_data.message_type.value
node_event = core_pb2.NodeEvent(message_type=message_type, node=node_proto)
return core_pb2.Event(node_event=node_event, source=node_data.source)
def handle_link_event(event: LinkData) -> core_pb2.LinkEvent: def handle_link_event(link_data: LinkData) -> core_pb2.Event:
""" """
Handle link event when there is a link event Handle link event when there is a link event
:param event: link data :param link_data: link data
:return: link event that has message type and link information :return: link event that has message type and link information
""" """
link = convert_link(event) link = convert_link(link_data)
return core_pb2.LinkEvent(message_type=event.message_type.value, link=link) message_type = link_data.message_type.value
link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
return core_pb2.Event(link_event=link_event, source=link_data.source)
def handle_session_event(event: EventData) -> core_pb2.SessionEvent: def handle_session_event(event_data: EventData) -> core_pb2.Event:
""" """
Handle session event when there is a session event Handle session event when there is a session event
:param event: event data :param event_data: event data
:return: session event :return: session event
""" """
event_time = event.time event_time = event_data.time
if event_time is not None: if event_time is not None:
event_time = float(event_time) event_time = float(event_time)
return core_pb2.SessionEvent( session_event = core_pb2.SessionEvent(
node_id=event.node, node_id=event_data.node,
event=event.event_type.value, event=event_data.event_type.value,
name=event.name, name=event_data.name,
data=event.data, data=event_data.data,
time=event_time, time=event_time,
) )
return core_pb2.Event(session_event=session_event)
def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent: def handle_config_event(config_data: ConfigData) -> core_pb2.Event:
""" """
Handle configuration event when there is configuration event Handle configuration event when there is configuration event
:param event: configuration data :param config_data: configuration data
:return: configuration event :return: configuration event
""" """
return core_pb2.ConfigEvent( config_event = core_pb2.ConfigEvent(
message_type=event.message_type, message_type=config_data.message_type,
node_id=event.node, node_id=config_data.node,
object=event.object, object=config_data.object,
type=event.type, type=config_data.type,
captions=event.captions, captions=config_data.captions,
bitmap=event.bitmap, bitmap=config_data.bitmap,
data_values=event.data_values, data_values=config_data.data_values,
possible_values=event.possible_values, possible_values=config_data.possible_values,
groups=event.groups, groups=config_data.groups,
interface=event.interface_number, iface_id=config_data.iface_id,
network_id=event.network_id, network_id=config_data.network_id,
opaque=event.opaque, opaque=config_data.opaque,
data_types=event.data_types, data_types=config_data.data_types,
) )
return core_pb2.Event(config_event=config_event)
def handle_exception_event(event: ExceptionData) -> core_pb2.ExceptionEvent: def handle_exception_event(exception_data: ExceptionData) -> core_pb2.Event:
""" """
Handle exception event when there is exception event Handle exception event when there is exception event
:param event: exception data :param exception_data: exception data
:return: exception event :return: exception event
""" """
return core_pb2.ExceptionEvent( exception_event = core_pb2.ExceptionEvent(
node_id=event.node, node_id=exception_data.node,
level=event.level.value, level=exception_data.level.value,
source=event.source, source=exception_data.source,
date=event.date, date=exception_data.date,
text=event.text, text=exception_data.text,
opaque=event.opaque, opaque=exception_data.opaque,
) )
return core_pb2.Event(exception_event=exception_event)
def handle_file_event(event: FileData) -> core_pb2.FileEvent: def handle_file_event(file_data: FileData) -> core_pb2.Event:
""" """
Handle file event Handle file event
:param event: file data :param file_data: file data
:return: file event :return: file event
""" """
return core_pb2.FileEvent( file_event = core_pb2.FileEvent(
message_type=event.message_type.value, message_type=file_data.message_type.value,
node_id=event.node, node_id=file_data.node,
name=event.name, name=file_data.name,
mode=event.mode, mode=file_data.mode,
number=event.number, number=file_data.number,
type=event.type, type=file_data.type,
source=event.source, source=file_data.source,
data=event.data, data=file_data.data,
compressed_data=event.compressed_data, compressed_data=file_data.compressed_data,
) )
return core_pb2.Event(file_event=file_event)
class EventStreamer: class EventStreamer:
@ -140,9 +152,9 @@ class EventStreamer:
:param session: session to process events for :param session: session to process events for
:param event_types: types of events to process :param event_types: types of events to process
""" """
self.session = session self.session: Session = session
self.event_types = event_types self.event_types: Iterable[core_pb2.EventType] = event_types
self.queue = Queue() self.queue: Queue = Queue()
self.add_handlers() self.add_handlers()
def add_handlers(self) -> None: def add_handlers(self) -> None:
@ -164,32 +176,33 @@ class EventStreamer:
if core_pb2.EventType.SESSION in self.event_types: if core_pb2.EventType.SESSION in self.event_types:
self.session.event_handlers.append(self.queue.put) self.session.event_handlers.append(self.queue.put)
def process(self) -> core_pb2.Event: def process(self) -> Optional[core_pb2.Event]:
""" """
Process the next event in the queue. Process the next event in the queue.
:return: grpc event, or None when invalid event or queue timeout :return: grpc event, or None when invalid event or queue timeout
""" """
event = core_pb2.Event(session_id=self.session.id) event = None
try: try:
data = self.queue.get(timeout=1) data = self.queue.get(timeout=1)
if isinstance(data, NodeData): if isinstance(data, NodeData):
event.node_event.CopyFrom(handle_node_event(data)) event = handle_node_event(data)
elif isinstance(data, LinkData): elif isinstance(data, LinkData):
event.link_event.CopyFrom(handle_link_event(data)) event = handle_link_event(data)
elif isinstance(data, EventData): elif isinstance(data, EventData):
event.session_event.CopyFrom(handle_session_event(data)) event = handle_session_event(data)
elif isinstance(data, ConfigData): elif isinstance(data, ConfigData):
event.config_event.CopyFrom(handle_config_event(data)) event = handle_config_event(data)
elif isinstance(data, ExceptionData): elif isinstance(data, ExceptionData):
event.exception_event.CopyFrom(handle_exception_event(data)) event = handle_exception_event(data)
elif isinstance(data, FileData): elif isinstance(data, FileData):
event.file_event.CopyFrom(handle_file_event(data)) event = handle_file_event(data)
else: else:
logging.error("unknown event: %s", data) logging.error("unknown event: %s", data)
event = None
except Empty: except Empty:
event = None pass
if event:
event.session_id = self.session.id
return event return event
def remove_handlers(self) -> None: def remove_handlers(self) -> None:

View file

@ -1,19 +1,19 @@
import logging import logging
import time import time
from typing import Any, Dict, List, Tuple, Type from typing import Any, Dict, List, Tuple, Type, Union
import netaddr import grpc
from grpc import ServicerContext
from core import utils from core import utils
from core.api.grpc import common_pb2, core_pb2 from core.api.grpc import common_pb2, core_pb2
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
from core.config import ConfigurableOptions from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, 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 NodeBase from core.nodes.base import CoreNode, 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
@ -29,17 +29,18 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
""" """
_id = node_proto.id _id = node_proto.id
_type = NodeTypes(node_proto.type) _type = NodeTypes(node_proto.type)
options = NodeOptions(name=node_proto.name, model=node_proto.model) options = NodeOptions(
options.icon = node_proto.icon name=node_proto.name,
options.opaque = node_proto.opaque model=node_proto.model,
options.image = node_proto.image icon=node_proto.icon,
options.services = node_proto.services image=node_proto.image,
options.config_services = node_proto.config_services services=node_proto.services,
config_services=node_proto.config_services,
)
if node_proto.emane: if node_proto.emane:
options.emane = node_proto.emane options.emane = node_proto.emane
if node_proto.server: if node_proto.server:
options.server = node_proto.server options.server = node_proto.server
position = node_proto.position position = node_proto.position
options.set_position(position.x, position.y) options.set_position(position.x, position.y)
if node_proto.HasField("geo"): if node_proto.HasField("geo"):
@ -48,66 +49,57 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
return _type, _id, options return _type, _id, options
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData: def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
""" """
Create interface data from interface proto. Create interface data from interface proto.
:param interface_proto: interface proto :param iface_proto: interface proto
:return: interface data :return: interface data
""" """
interface = None iface_data = None
if interface_proto: if iface_proto:
name = interface_proto.name name = iface_proto.name if iface_proto.name else None
if name == "": mac = iface_proto.mac if iface_proto.mac else None
name = None ip4 = iface_proto.ip4 if iface_proto.ip4 else None
mac = interface_proto.mac ip6 = iface_proto.ip6 if iface_proto.ip6 else None
if mac == "": iface_data = InterfaceData(
mac = None id=iface_proto.id,
interface = InterfaceData(
_id=interface_proto.id,
name=name, name=name,
mac=mac, mac=mac,
ip4=interface_proto.ip4, ip4=ip4,
ip4_mask=interface_proto.ip4mask, ip4_mask=iface_proto.ip4_mask,
ip6=interface_proto.ip6, ip6=ip6,
ip6_mask=interface_proto.ip6mask, ip6_mask=iface_proto.ip6_mask,
) )
return interface return iface_data
def add_link_data( def add_link_data(
link_proto: core_pb2.Link link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]: ) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
""" """
Convert link proto to link interfaces and options data. Convert link proto to link interfaces and options data.
:param link_proto: link proto :param link_proto: link proto
:return: link interfaces and options :return: link interfaces and options
""" """
interface_one = link_interface(link_proto.interface_one) iface1_data = link_iface(link_proto.iface1)
interface_two = link_interface(link_proto.interface_two) iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type)
link_type = None options = LinkOptions()
link_type_value = link_proto.type options_proto = link_proto.options
if link_type_value is not None: if options_proto:
link_type = LinkTypes(link_type_value) options.delay = options_proto.delay
options.bandwidth = options_proto.bandwidth
options = LinkOptions(_type=link_type) options.loss = options_proto.loss
options_data = link_proto.options options.dup = options_proto.dup
if options_data: options.jitter = options_proto.jitter
options.delay = options_data.delay options.mer = options_proto.mer
options.bandwidth = options_data.bandwidth options.burst = options_proto.burst
options.per = options_data.per options.mburst = options_proto.mburst
options.dup = options_data.dup options.unidirectional = options_proto.unidirectional
options.jitter = options_data.jitter options.key = options_proto.key
options.mer = options_data.mer return iface1_data, iface2_data, options, link_type
options.burst = options_data.burst
options.mburst = options_data.mburst
options.unidirectional = options_data.unidirectional
options.key = options_data.key
options.opaque = options_data.opaque
return interface_one, interface_two, options
def create_nodes( def create_nodes(
@ -145,10 +137,10 @@ def create_links(
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
node_one_id = link_proto.node_one_id node1_id = link_proto.node1_id
node_two_id = link_proto.node_two_id node2_id = link_proto.node2_id
interface_one, interface_two, options = add_link_data(link_proto) iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one, interface_two, options) args = (node1_id, node2_id, iface1, iface2, options, link_type)
funcs.append((session.add_link, args, {})) funcs.append((session.add_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -169,10 +161,10 @@ def edit_links(
""" """
funcs = [] funcs = []
for link_proto in link_protos: for link_proto in link_protos:
node_one_id = link_proto.node_one_id node1_id = link_proto.node1_id
node_two_id = link_proto.node_two_id node2_id = link_proto.node2_id
interface_one, interface_two, options = add_link_data(link_proto) iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
funcs.append((session.update_link, args, {})) funcs.append((session.update_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -194,7 +186,8 @@ def convert_value(value: Any) -> str:
def get_config_options( def get_config_options(
config: Dict[str, str], configurable_options: Type[ConfigurableOptions] config: Dict[str, str],
configurable_options: Union[ConfigurableOptions, Type[ConfigurableOptions]],
) -> Dict[str, common_pb2.ConfigOption]: ) -> Dict[str, common_pb2.ConfigOption]:
""" """
Retrieve configuration options in a form that is used by the grpc server. Retrieve configuration options in a form that is used by the grpc server.
@ -276,22 +269,22 @@ def get_links(node: NodeBase):
:return: protobuf links :return: protobuf links
""" """
links = [] links = []
for link_data in node.all_link_data(): for link in node.links():
link = convert_link(link_data) link_proto = convert_link(link)
links.append(link) links.append(link_proto)
return links return links
def get_emane_model_id(node_id: int, interface_id: int) -> int: def get_emane_model_id(node_id: int, iface_id: int) -> int:
""" """
Get EMANE model id Get EMANE model id
:param node_id: node id :param node_id: node id
:param interface_id: interface id :param iface_id: interface id
:return: EMANE model id :return: EMANE model id
""" """
if interface_id >= 0: if iface_id >= 0:
return node_id * 1000 + interface_id return node_id * 1000 + iface_id
else: else:
return node_id return node_id
@ -303,12 +296,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
:param _id: id to parse :param _id: id to parse
:return: node id and interface id :return: node id and interface id
""" """
interface = -1 iface_id = -1
node_id = _id node_id = _id
if _id >= 1000: if _id >= 1000:
interface = _id % 1000 iface_id = _id % 1000
node_id = int(_id / 1000) node_id = int(_id / 1000)
return node_id, interface return node_id, iface_id
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
return core_pb2.Interface(
id=iface_data.id,
name=iface_data.name,
mac=iface_data.mac,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
)
def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
return core_pb2.LinkOptions(
jitter=options_data.jitter,
key=options_data.key,
mburst=options_data.mburst,
mer=options_data.mer,
loss=options_data.loss,
bandwidth=options_data.bandwidth,
burst=options_data.burst,
delay=options_data.delay,
dup=options_data.dup,
unidirectional=options_data.unidirectional,
)
def convert_link(link_data: LinkData) -> core_pb2.Link: def convert_link(link_data: LinkData) -> core_pb2.Link:
@ -318,47 +338,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
:param link_data: link to convert :param link_data: link to convert
:return: core protobuf Link :return: core protobuf Link
""" """
interface_one = None iface1 = None
if link_data.interface1_id is not None: if link_data.iface1 is not None:
interface_one = core_pb2.Interface( iface1 = convert_iface(link_data.iface1)
id=link_data.interface1_id, iface2 = None
name=link_data.interface1_name, if link_data.iface2 is not None:
mac=convert_value(link_data.interface1_mac), iface2 = convert_iface(link_data.iface2)
ip4=convert_value(link_data.interface1_ip4), options = convert_link_options(link_data.options)
ip4mask=link_data.interface1_ip4_mask,
ip6=convert_value(link_data.interface1_ip6),
ip6mask=link_data.interface1_ip6_mask,
)
interface_two = None
if link_data.interface2_id is not None:
interface_two = core_pb2.Interface(
id=link_data.interface2_id,
name=link_data.interface2_name,
mac=convert_value(link_data.interface2_mac),
ip4=convert_value(link_data.interface2_ip4),
ip4mask=link_data.interface2_ip4_mask,
ip6=convert_value(link_data.interface2_ip6),
ip6mask=link_data.interface2_ip6_mask,
)
options = core_pb2.LinkOptions(
opaque=link_data.opaque,
jitter=link_data.jitter,
key=link_data.key,
mburst=link_data.mburst,
mer=link_data.mer,
per=link_data.per,
bandwidth=link_data.bandwidth,
burst=link_data.burst,
delay=link_data.delay,
dup=link_data.dup,
unidirectional=link_data.unidirectional,
)
return core_pb2.Link( return core_pb2.Link(
type=link_data.link_type.value, type=link_data.type.value,
node_one_id=link_data.node1_id, node1_id=link_data.node1_id,
node_two_id=link_data.node2_id, node2_id=link_data.node2_id,
interface_one=interface_one, iface1=iface1,
interface_two=interface_two, iface2=iface2,
options=options, options=options,
network_id=link_data.network_id, network_id=link_data.network_id,
label=link_data.label, label=link_data.label,
@ -422,7 +414,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None:
service.shutdown = tuple(config.shutdown) service.shutdown = tuple(config.shutdown)
def get_service_configuration(service: Type[CoreService]) -> NodeServiceData: def get_service_configuration(service: CoreService) -> NodeServiceData:
""" """
Convenience for converting a service to service data proto. Convenience for converting a service to service data proto.
@ -443,38 +435,84 @@ def get_service_configuration(service: Type[CoreService]) -> NodeServiceData:
) )
def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface: def iface_to_data(iface: CoreInterface) -> InterfaceData:
ip4 = iface.get_ip4()
ip4_addr = str(ip4.ip) if ip4 else None
ip4_mask = ip4.prefixlen if ip4 else None
ip6 = iface.get_ip6()
ip6_addr = str(ip6.ip) if ip6 else None
ip6_mask = ip6.prefixlen if ip6 else None
return InterfaceData(
id=iface.node_id,
name=iface.name,
mac=str(iface.mac),
ip4=ip4_addr,
ip4_mask=ip4_mask,
ip6=ip6_addr,
ip6_mask=ip6_mask,
)
def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
""" """
Convenience for converting a core interface to the protobuf representation. Convenience for converting a core interface to the protobuf representation.
:param interface: interface to convert
:param node_id: id of node to convert interface for
:param iface: interface to convert
:return: interface proto :return: interface proto
""" """
net_id = None if iface.node and iface.node.id == node_id:
if interface.net: _id = iface.node_id
net_id = interface.net.id else:
ip4 = None _id = iface.net_id
ip4mask = None net_id = iface.net.id if iface.net else None
ip6 = None node_id = iface.node.id if iface.node else None
ip6mask = None net2_id = iface.othernet.id if iface.othernet else None
for addr in interface.addrlist: ip4_net = iface.get_ip4()
network = netaddr.IPNetwork(addr) ip4 = str(ip4_net.ip) if ip4_net else None
mask = network.prefixlen ip4_mask = ip4_net.prefixlen if ip4_net else None
ip = str(network.ip) ip6_net = iface.get_ip6()
if netaddr.valid_ipv4(ip) and not ip4: ip6 = str(ip6_net.ip) if ip6_net else None
ip4 = ip ip6_mask = ip6_net.prefixlen if ip6_net else None
ip4mask = mask mac = str(iface.mac) if iface.mac else None
elif netaddr.valid_ipv6(ip) and not ip6:
ip6 = ip
ip6mask = mask
return core_pb2.Interface( return core_pb2.Interface(
id=interface.netindex, id=_id,
netid=net_id, net_id=net_id,
name=interface.name, net2_id=net2_id,
mac=str(interface.hwaddr), node_id=node_id,
mtu=interface.mtu, name=iface.name,
flowid=interface.flow_id, mac=mac,
mtu=iface.mtu,
flow_id=iface.flow_id,
ip4=ip4, ip4=ip4,
ip4mask=ip4mask, ip4_mask=ip4_mask,
ip6=ip6, ip6=ip6,
ip6mask=ip6mask, ip6_mask=ip6_mask,
) )
def get_nem_id(
session: Session, node: CoreNode, iface_id: int, context: ServicerContext
) -> int:
"""
Get nem id for a given node and interface id.
:param session: session node belongs to
:param node: node to get nem id for
:param iface_id: id of interface on node to get nem id for
:param context: request context
:return: nem id
"""
iface = node.ifaces.get(iface_id)
if not iface:
message = f"{node.name} missing interface {iface_id}"
context.abort(grpc.StatusCode.NOT_FOUND, message)
net = iface.net
if not isinstance(net, EmaneNet):
message = f"{node.name} interface {iface_id} is not an EMANE network"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
nem_id = session.emane.get_nem_id(iface)
if nem_id is None:
message = f"{node.name} interface {iface_id} nem id does not exist"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
return nem_id

View file

@ -6,7 +6,7 @@ import tempfile
import threading import threading
import time import time
from concurrent import futures from concurrent import futures
from typing import Type from typing import Iterable, Optional, Pattern, Type
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -39,6 +39,8 @@ from core.api.grpc.core_pb2 import ExecuteScriptResponse
from core.api.grpc.emane_pb2 import ( from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
EmaneLinkResponse, EmaneLinkResponse,
EmanePathlossesRequest,
EmanePathlossesResponse,
GetEmaneConfigRequest, GetEmaneConfigRequest,
GetEmaneConfigResponse, GetEmaneConfigResponse,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
@ -106,18 +108,17 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkResponse, WlanLinkResponse,
) )
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.emudata import LinkOptions, NodeOptions
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
from core.emulator.session import NT, Session from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import PtpNet, WlanNode
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
_ONE_DAY_IN_SECONDS = 60 * 60 * 24 _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX = re.compile(r"veth(?P<node>[0-9a-fA-F]+)") _INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -129,9 +130,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def __init__(self, coreemu: CoreEmu) -> None: def __init__(self, coreemu: CoreEmu) -> None:
super().__init__() super().__init__()
self.coreemu = coreemu self.coreemu: CoreEmu = coreemu
self.running = True self.running: bool = True
self.server = None self.server: Optional[grpc.Server] = None
atexit.register(self._exit_handler) atexit.register(self._exit_handler)
def _exit_handler(self) -> None: def _exit_handler(self) -> None:
@ -244,7 +245,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config = session.emane.get_configs() config = session.emane.get_configs()
config.update(request.emane_config) config.update(request.emane_config)
for config in request.emane_model_configs: for config in request.emane_model_configs:
_id = get_emane_model_id(config.node_id, config.interface_id) _id = get_emane_model_id(config.node_id, config.iface_id)
session.emane.set_model_config(_id, config.model, config.config) session.emane.set_model_config(_id, config.model, config.config)
# wlan configs # wlan configs
@ -542,8 +543,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes = [] nodes = []
for _id in session.nodes: for _id in session.nodes:
node = session.nodes[_id] node = session.nodes[_id]
if not isinstance(node.id, int): if not isinstance(node, PtpNet):
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(node) node_links = get_links(node)
@ -623,16 +623,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
key = key.split(".") key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = _INTERFACE_REGEX.search(key[0]).group("node")
node_id = int(node_id, base=16) node_id = int(node_id, base=16)
interface_id = int(key[1], base=16) iface_id = int(key[1], base=16)
session_id = int(key[2], base=16) session_id = int(key[2], base=16)
if session.id != session_id: if session.id != session_id:
continue continue
interface_throughput = ( iface_throughput = throughputs_event.iface_throughputs.add()
throughputs_event.interface_throughputs.add() iface_throughput.node_id = node_id
) iface_throughput.iface_id = iface_id
interface_throughput.node_id = node_id iface_throughput.throughput = throughput
interface_throughput.interface_id = interface_id
interface_throughput.throughput = throughput
elif key.startswith("b."): elif key.startswith("b."):
try: try:
key = key.split(".") key = key.split(".")
@ -669,6 +667,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
_type, _id, options = grpcutils.add_node_data(request.node) _type, _id, options = grpcutils.add_node_data(request.node)
_class = session.get_node_class(_type) _class = session.get_node_class(_type)
node = session.add_node(_class, _id, options) node = session.add_node(_class, _id, options)
source = request.source if request.source else None
session.broadcast_node(node, MessageFlags.ADD, source)
return core_pb2.AddNodeResponse(node_id=node.id) return core_pb2.AddNodeResponse(node_id=node.id)
def GetNode( def GetNode(
@ -684,13 +684,49 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node: %s", request) logging.debug("get node: %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, NodeBase) node = self.get_node(session, request.node_id, context, NodeBase)
interfaces = [] ifaces = []
for interface_id in node._netif: for iface_id in node.ifaces:
interface = node._netif[interface_id] iface = node.ifaces[iface_id]
interface_proto = grpcutils.interface_to_proto(interface) iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
interfaces.append(interface_proto) ifaces.append(iface_proto)
node_proto = grpcutils.get_node_proto(session, node) node_proto = grpcutils.get_node_proto(session, node)
return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces)
def MoveNodes(
self,
request_iterator: Iterable[core_pb2.MoveNodesRequest],
context: ServicerContext,
) -> core_pb2.MoveNodesResponse:
"""
Stream node movements
:param request_iterator: move nodes request iterator
:param context: context object
:return: move nodes response
"""
for request in request_iterator:
if not request.WhichOneof("move_type"):
raise CoreError("move nodes must provide a move type")
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, NodeBase)
options = NodeOptions()
has_geo = request.HasField("geo")
if has_geo:
logging.info("has geo")
lat = request.geo.lat
lon = request.geo.lon
alt = request.geo.alt
options.set_location(lat, lon, alt)
else:
x = request.position.x
y = request.position.y
logging.info("has pos: %s,%s", x, y)
options.set_position(x, y)
session.edit_node(node.id, options)
source = request.source if request.source else None
if not has_geo:
session.broadcast_node(node, source=source)
return core_pb2.MoveNodesResponse()
def EditNode( def EditNode(
self, request: core_pb2.EditNodeRequest, context: ServicerContext self, request: core_pb2.EditNodeRequest, context: ServicerContext
@ -705,8 +741,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("edit node: %s", request) logging.debug("edit node: %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, NodeBase) node = self.get_node(session, request.node_id, context, NodeBase)
options = NodeOptions() options = NodeOptions(icon=request.icon)
options.icon = request.icon
if request.HasField("position"): if request.HasField("position"):
x = request.position.x x = request.position.x
y = request.position.y y = request.position.y
@ -741,7 +776,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("delete node: %s", request) logging.debug("delete node: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
result = session.delete_node(request.node_id) result = False
if request.node_id in session.nodes:
node = self.get_node(session, request.node_id, context, NodeBase)
result = session.delete_node(node.id)
source = request.source if request.source else None
session.broadcast_node(node, MessageFlags.DELETE, source)
return core_pb2.DeleteNodeResponse(result=result) return core_pb2.DeleteNodeResponse(result=result)
def NodeCommand( def NodeCommand(
@ -758,10 +798,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
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, CoreNode) node = self.get_node(session, request.node_id, context, CoreNode)
try: try:
output = node.cmd(request.command) output = node.cmd(request.command, request.wait, request.shell)
return_code = 0
except CoreCommandError as e: except CoreCommandError as e:
output = e.stderr output = e.stderr
return core_pb2.NodeCommandResponse(output=output) return_code = e.returncode
return core_pb2.NodeCommandResponse(output=output, return_code=return_code)
def GetNodeTerminal( def GetNodeTerminal(
self, request: core_pb2.GetNodeTerminalRequest, context: ServicerContext self, request: core_pb2.GetNodeTerminalRequest, context: ServicerContext
@ -806,27 +848,42 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: add-link response :return: add-link response
""" """
logging.debug("add link: %s", request) logging.debug("add link: %s", request)
# validate session and nodes
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
self.get_node(session, request.link.node_one_id, context, NodeBase) node1_id = request.link.node1_id
self.get_node(session, request.link.node_two_id, context, NodeBase) node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase)
node_one_id = request.link.node_one_id self.get_node(session, node2_id, context, NodeBase)
node_two_id = request.link.node_two_id iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
interface_one, interface_two, options = grpcutils.add_link_data(request.link) request.link
node_one_interface, node_two_interface = session.add_link(
node_one_id, node_two_id, interface_one, interface_two, link_options=options
) )
interface_one_proto = None node1_iface, node2_iface = session.add_link(
interface_two_proto = None node1_id, node2_id, iface1_data, iface2_data, options, link_type
if node_one_interface: )
interface_one_proto = grpcutils.interface_to_proto(node_one_interface) iface1_data = None
if node_two_interface: if node1_iface:
interface_two_proto = grpcutils.interface_to_proto(node_two_interface) iface1_data = grpcutils.iface_to_data(node1_iface)
iface2_data = None
if node2_iface:
iface2_data = grpcutils.iface_to_data(node2_iface)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.ADD,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1_data,
iface2=iface2_data,
options=options,
source=source,
)
session.broadcast_link(link_data)
iface1_proto = None
iface2_proto = None
if node1_iface:
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface)
if node2_iface:
iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface)
return core_pb2.AddLinkResponse( return core_pb2.AddLinkResponse(
result=True, result=True, iface1=iface1_proto, iface2=iface2_proto
interface_one=interface_one_proto,
interface_two=interface_two_proto,
) )
def EditLink( def EditLink(
@ -841,26 +898,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("edit link: %s", request) logging.debug("edit link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id node1_id = request.node1_id
node_two_id = request.node_two_id node2_id = request.node2_id
interface_one_id = request.interface_one_id iface1_id = request.iface1_id
interface_two_id = request.interface_two_id iface2_id = request.iface2_id
options_data = request.options options_proto = request.options
link_options = LinkOptions() options = LinkOptions(
link_options.delay = options_data.delay delay=options_proto.delay,
link_options.bandwidth = options_data.bandwidth bandwidth=options_proto.bandwidth,
link_options.per = options_data.per loss=options_proto.loss,
link_options.dup = options_data.dup dup=options_proto.dup,
link_options.jitter = options_data.jitter jitter=options_proto.jitter,
link_options.mer = options_data.mer mer=options_proto.mer,
link_options.burst = options_data.burst burst=options_proto.burst,
link_options.mburst = options_data.mburst mburst=options_proto.mburst,
link_options.unidirectional = options_data.unidirectional unidirectional=options_proto.unidirectional,
link_options.key = options_data.key key=options_proto.key,
link_options.opaque = options_data.opaque
session.update_link(
node_one_id, node_two_id, interface_one_id, interface_two_id, link_options
) )
session.update_link(node1_id, node2_id, iface1_id, iface2_id, options)
iface1 = InterfaceData(id=iface1_id)
iface2 = InterfaceData(id=iface2_id)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.NONE,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1,
iface2=iface2,
options=options,
source=source,
)
session.broadcast_link(link_data)
return core_pb2.EditLinkResponse(result=True) return core_pb2.EditLinkResponse(result=True)
def DeleteLink( def DeleteLink(
@ -875,13 +943,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("delete link: %s", request) logging.debug("delete link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id node1_id = request.node1_id
node_two_id = request.node_two_id node2_id = request.node2_id
interface_one_id = request.interface_one_id iface1_id = request.iface1_id
interface_two_id = request.interface_two_id iface2_id = request.iface2_id
session.delete_link( session.delete_link(node1_id, node2_id, iface1_id, iface2_id)
node_one_id, node_two_id, interface_one_id, interface_two_id iface1 = InterfaceData(id=iface1_id)
iface2 = InterfaceData(id=iface2_id)
source = request.source if request.source else None
link_data = LinkData(
message_type=MessageFlags.DELETE,
node1_id=node1_id,
node2_id=node2_id,
iface1=iface1,
iface2=iface2,
source=source,
) )
session.broadcast_link(link_data)
return core_pb2.DeleteLinkResponse(result=True) return core_pb2.DeleteLinkResponse(result=True)
def GetHooks( def GetHooks(
@ -897,8 +975,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get hooks: %s", request) logging.debug("get hooks: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
hooks = [] hooks = []
for state in session._hooks: for state in session.hooks:
state_hooks = session._hooks[state] state_hooks = session.hooks[state]
for file_name, file_data in state_hooks: for file_name, file_data in state_hooks:
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
hooks.append(hook) hooks.append(hook)
@ -1265,13 +1343,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("set wlan config: %s", request) logging.debug("set wlan config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
wlan_config = request.wlan_config node_id = request.wlan_config.node_id
session.mobility.set_model_config( config = request.wlan_config.config
wlan_config.node_id, BasicRangeModel.name, wlan_config.config session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
)
if session.state == EventTypes.RUNTIME_STATE: if session.state == EventTypes.RUNTIME_STATE:
node = self.get_node(session, wlan_config.node_id, context, WlanNode) node = self.get_node(session, node_id, context, WlanNode)
node.updatemodel(wlan_config.config) node.updatemodel(config)
return SetWlanConfigResponse(result=True) return SetWlanConfigResponse(result=True)
def GetEmaneConfig( def GetEmaneConfig(
@ -1339,7 +1416,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get emane model config: %s", request) logging.debug("get emane model config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
model = session.emane.models[request.model] model = session.emane.models[request.model]
_id = get_emane_model_id(request.node_id, request.interface) _id = get_emane_model_id(request.node_id, request.iface_id)
current_config = session.emane.get_model_config(_id, request.model) current_config = session.emane.get_model_config(_id, request.model)
config = get_config_options(current_config, model) config = get_config_options(current_config, model)
return GetEmaneModelConfigResponse(config=config) return GetEmaneModelConfigResponse(config=config)
@ -1358,7 +1435,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("set emane model config: %s", request) logging.debug("set emane model config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
model_config = request.emane_model_config model_config = request.emane_model_config
_id = get_emane_model_id(model_config.node_id, model_config.interface_id) _id = get_emane_model_id(model_config.node_id, model_config.iface_id)
session.emane.set_model_config(_id, model_config.model, model_config.config) session.emane.set_model_config(_id, model_config.model, model_config.config)
return SetEmaneModelConfigResponse(result=True) return SetEmaneModelConfigResponse(result=True)
@ -1387,12 +1464,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
model = session.emane.models[model_name] model = session.emane.models[model_name]
current_config = session.emane.get_model_config(_id, model_name) current_config = session.emane.get_model_config(_id, model_name)
config = get_config_options(current_config, model) config = get_config_options(current_config, model)
node_id, interface = grpcutils.parse_emane_model_id(_id) node_id, iface_id = grpcutils.parse_emane_model_id(_id)
model_config = GetEmaneModelConfigsResponse.ModelConfig( model_config = GetEmaneModelConfigsResponse.ModelConfig(
node_id=node_id, node_id=node_id, model=model_name, iface_id=iface_id, config=config
model=model_name,
interface=interface,
config=config,
) )
configs.append(model_config) configs.append(model_config)
return GetEmaneModelConfigsResponse(configs=configs) return GetEmaneModelConfigsResponse(configs=configs)
@ -1457,16 +1531,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:param context: context object :param context: context object
:return: get-interfaces response that has all the system's interfaces :return: get-interfaces response that has all the system's interfaces
""" """
interfaces = [] ifaces = []
for interface in os.listdir("/sys/class/net"): for iface in os.listdir("/sys/class/net"):
if ( if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
interface.startswith("b.")
or interface.startswith("veth")
or interface == "lo"
):
continue continue
interfaces.append(interface) ifaces.append(iface)
return core_pb2.GetInterfacesResponse(interfaces=interfaces) return core_pb2.GetInterfacesResponse(ifaces=ifaces)
def EmaneLink( def EmaneLink(
self, request: EmaneLinkRequest, context: ServicerContext self, request: EmaneLinkRequest, context: ServicerContext
@ -1480,30 +1550,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("emane link: %s", request) logging.debug("emane link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
nem_one = request.nem_one nem1 = request.nem1
emane_one, netif = session.emane.nemlookup(nem_one) iface1 = session.emane.get_iface(nem1)
if not emane_one or not netif: if not iface1:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found") context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
node_one = netif.node node1 = iface1.node
nem_two = request.nem_two nem2 = request.nem2
emane_two, netif = session.emane.nemlookup(nem_two) iface2 = session.emane.get_iface(nem2)
if not emane_two or not netif: if not iface2:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found") context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
node_two = netif.node node2 = iface2.node
if emane_one.id == emane_two.id: if iface1.net == iface2.net:
if request.linked: if request.linked:
flag = MessageFlags.ADD flag = MessageFlags.ADD
else: else:
flag = MessageFlags.DELETE flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id) color = session.get_link_color(iface1.net.id)
link = LinkData( link = LinkData(
message_type=flag, message_type=flag,
link_type=LinkTypes.WIRELESS, type=LinkTypes.WIRELESS,
node1_id=node_one.id, node1_id=node1.id,
node2_id=node_two.id, node2_id=node2.id,
network_id=emane_one.id, network_id=iface1.net.id,
color=color, color=color,
) )
session.broadcast_link(link) session.broadcast_link(link)
@ -1700,20 +1770,34 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
grpc.StatusCode.NOT_FOUND, grpc.StatusCode.NOT_FOUND,
f"wlan node {request.wlan} does not using BasicRangeModel", f"wlan node {request.wlan} does not using BasicRangeModel",
) )
n1 = self.get_node(session, request.node_one, context, CoreNode) node1 = self.get_node(session, request.node1_id, context, CoreNode)
n2 = self.get_node(session, request.node_two, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode)
n1_netif, n2_netif = None, None node1_iface, node2_iface = None, None
for net, netif1, netif2 in n1.commonnets(n2): for net, iface1, iface2 in node1.commonnets(node2):
if net == wlan: if net == wlan:
n1_netif = netif1 node1_iface = iface1
n2_netif = netif2 node2_iface = iface2
break break
result = False result = False
if n1_netif and n2_netif: if node1_iface and node2_iface:
if request.linked: if request.linked:
wlan.link(n1_netif, n2_netif) wlan.link(node1_iface, node2_iface)
else: else:
wlan.unlink(n1_netif, n2_netif) wlan.unlink(node1_iface, node2_iface)
wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked) wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
result = True result = True
return WlanLinkResponse(result=result) return WlanLinkResponse(result=result)
def EmanePathlosses(
self,
request_iterator: Iterable[EmanePathlossesRequest],
context: ServicerContext,
) -> EmanePathlossesResponse:
for request in request_iterator:
session = self.get_session(request.session_id, context)
node1 = self.get_node(session, request.node1_id, context, CoreNode)
nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context)
node2 = self.get_node(session, request.node2_id, context, CoreNode)
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
return EmanePathlossesResponse()

View file

@ -495,7 +495,7 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32, LinkTlvs.N2_NUMBER.value: CoreTlvDataUint32,
LinkTlvs.DELAY.value: CoreTlvDataUint64, LinkTlvs.DELAY.value: CoreTlvDataUint64,
LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64, LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64,
LinkTlvs.PER.value: CoreTlvDataString, LinkTlvs.LOSS.value: CoreTlvDataString,
LinkTlvs.DUP.value: CoreTlvDataString, LinkTlvs.DUP.value: CoreTlvDataString,
LinkTlvs.JITTER.value: CoreTlvDataUint64, LinkTlvs.JITTER.value: CoreTlvDataUint64,
LinkTlvs.MER.value: CoreTlvDataUint16, LinkTlvs.MER.value: CoreTlvDataUint16,
@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32, LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32, LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
LinkTlvs.KEY.value: CoreTlvDataUint32, LinkTlvs.KEY.value: CoreTlvDataUint32,
LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16, LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr, LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr, LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr, LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16, LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr, LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr, LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr, LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString, LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString, LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
LinkTlvs.OPAQUE.value: CoreTlvDataString, LinkTlvs.OPAQUE.value: CoreTlvDataString,
@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv):
ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString, ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
ConfigTlvs.GROUPS.value: CoreTlvDataString, ConfigTlvs.GROUPS.value: CoreTlvDataString,
ConfigTlvs.SESSION.value: CoreTlvDataString, ConfigTlvs.SESSION.value: CoreTlvDataString,
ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16, ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16,
ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32, ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
ConfigTlvs.OPAQUE.value: CoreTlvDataString, ConfigTlvs.OPAQUE.value: CoreTlvDataString,
} }

View file

@ -12,6 +12,7 @@ import threading
import time import time
from itertools import repeat from itertools import repeat
from queue import Empty, Queue from queue import Empty, Queue
from typing import Optional
from core import utils from core import utils
from core.api.tlv import coreapi, dataconversion, structutils from core.api.tlv import coreapi, dataconversion, structutils
@ -28,8 +29,15 @@ from core.api.tlv.enumerations import (
NodeTlvs, NodeTlvs,
SessionTlvs, SessionTlvs,
) )
from core.emulator.data import ConfigData, EventData, ExceptionData, FileData from core.emulator.data import (
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions ConfigData,
EventData,
ExceptionData,
FileData,
InterfaceData,
LinkOptions,
NodeOptions,
)
from core.emulator.enumerations import ( from core.emulator.enumerations import (
ConfigDataTypes, ConfigDataTypes,
EventTypes, EventTypes,
@ -39,6 +47,7 @@ from core.emulator.enumerations import (
NodeTypes, NodeTypes,
RegisterTlvs, RegisterTlvs,
) )
from core.emulator.session import Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel from core.location.mobility import BasicRangeModel
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
@ -69,7 +78,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.REGISTER.value: self.handle_register_message,
MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.CONFIG.value: self.handle_config_message,
MessageTypes.FILE.value: self.handle_file_message, MessageTypes.FILE.value: self.handle_file_message,
MessageTypes.INTERFACE.value: self.handle_interface_message, MessageTypes.INTERFACE.value: self.handle_iface_message,
MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.EVENT.value: self.handle_event_message,
MessageTypes.SESSION.value: self.handle_session_message, MessageTypes.SESSION.value: self.handle_session_message,
} }
@ -79,17 +88,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
self._sessions_lock = threading.Lock() self._sessions_lock = threading.Lock()
self.handler_threads = [] self.handler_threads = []
num_threads = int(server.config["numthreads"]) thread = threading.Thread(target=self.handler_thread, daemon=True)
if num_threads < 1:
raise ValueError(f"invalid number of threads: {num_threads}")
logging.debug("launching core server handler threads: %s", num_threads)
for _ in range(num_threads):
thread = threading.Thread(target=self.handler_thread)
self.handler_threads.append(thread)
thread.start() thread.start()
self.handler_threads.append(thread)
self.session = None self.session: Optional[Session] = None
self.coreemu = server.coreemu self.coreemu = server.coreemu
utils.close_onexec(request.fileno()) utils.close_onexec(request.fileno())
socketserver.BaseRequestHandler.__init__(self, request, client_address, server) socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
@ -182,7 +185,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
node_count_list.append(str(session.get_node_count())) node_count_list.append(str(session.get_node_count()))
date_list.append(time.ctime(session._state_time)) date_list.append(time.ctime(session.state_time))
thumb = session.thumbnail thumb = session.thumbnail
if not thumb: if not thumb:
@ -326,7 +329,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
""" """
logging.debug("handling broadcast node: %s", node_data) logging.debug("handling broadcast node: %s", node_data)
message = dataconversion.convert_node(node_data) message = dataconversion.convert_node(node_data)
try: try:
self.sendall(message) self.sendall(message)
except IOError: except IOError:
@ -340,46 +342,49 @@ class CoreHandler(socketserver.BaseRequestHandler):
:return: nothing :return: nothing
""" """
logging.debug("handling broadcast link: %s", link_data) logging.debug("handling broadcast link: %s", link_data)
per = "" options_data = link_data.options
if link_data.per is not None: loss = ""
per = str(link_data.per) if options_data.loss is not None:
loss = str(options_data.loss)
dup = "" dup = ""
if link_data.dup is not None: if options_data.dup is not None:
dup = str(link_data.dup) dup = str(options_data.dup)
iface1 = link_data.iface1
if iface1 is None:
iface1 = InterfaceData()
iface2 = link_data.iface2
if iface2 is None:
iface2 = InterfaceData()
tlv_data = structutils.pack_values( tlv_data = structutils.pack_values(
coreapi.CoreLinkTlv, coreapi.CoreLinkTlv,
[ [
(LinkTlvs.N1_NUMBER, link_data.node1_id), (LinkTlvs.N1_NUMBER, link_data.node1_id),
(LinkTlvs.N2_NUMBER, link_data.node2_id), (LinkTlvs.N2_NUMBER, link_data.node2_id),
(LinkTlvs.DELAY, link_data.delay), (LinkTlvs.DELAY, options_data.delay),
(LinkTlvs.BANDWIDTH, link_data.bandwidth), (LinkTlvs.BANDWIDTH, options_data.bandwidth),
(LinkTlvs.PER, per), (LinkTlvs.LOSS, loss),
(LinkTlvs.DUP, dup), (LinkTlvs.DUP, dup),
(LinkTlvs.JITTER, link_data.jitter), (LinkTlvs.JITTER, options_data.jitter),
(LinkTlvs.MER, link_data.mer), (LinkTlvs.MER, options_data.mer),
(LinkTlvs.BURST, link_data.burst), (LinkTlvs.BURST, options_data.burst),
(LinkTlvs.SESSION, link_data.session), (LinkTlvs.MBURST, options_data.mburst),
(LinkTlvs.MBURST, link_data.mburst), (LinkTlvs.TYPE, link_data.type.value),
(LinkTlvs.TYPE, link_data.link_type.value), (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional),
(LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes),
(LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional),
(LinkTlvs.EMULATION_ID, link_data.emulation_id),
(LinkTlvs.NETWORK_ID, link_data.network_id), (LinkTlvs.NETWORK_ID, link_data.network_id),
(LinkTlvs.KEY, link_data.key), (LinkTlvs.KEY, options_data.key),
(LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id), (LinkTlvs.IFACE1_NUMBER, iface1.id),
(LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4), (LinkTlvs.IFACE1_IP4, iface1.ip4),
(LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask), (LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask),
(LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac), (LinkTlvs.IFACE1_MAC, iface1.mac),
(LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6), (LinkTlvs.IFACE1_IP6, iface1.ip6),
(LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask), (LinkTlvs.IFACE1_IP6_MASK, iface1.ip6_mask),
(LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id), (LinkTlvs.IFACE2_NUMBER, iface2.id),
(LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4), (LinkTlvs.IFACE2_IP4, iface2.ip4),
(LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask), (LinkTlvs.IFACE2_IP4_MASK, iface2.ip4_mask),
(LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac), (LinkTlvs.IFACE2_MAC, iface2.mac),
(LinkTlvs.INTERFACE2_IP6, link_data.interface2_ip6), (LinkTlvs.IFACE2_IP6, iface2.ip6),
(LinkTlvs.INTERFACE2_IP6_MASK, link_data.interface2_ip6_mask), (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask),
(LinkTlvs.OPAQUE, link_data.opaque),
], ],
) )
@ -713,7 +718,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
options.icon = message.get_tlv(NodeTlvs.ICON.value) options.icon = message.get_tlv(NodeTlvs.ICON.value)
options.canvas = message.get_tlv(NodeTlvs.CANVAS.value) options.canvas = message.get_tlv(NodeTlvs.CANVAS.value)
options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value)
options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value) options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value)
services = message.get_tlv(NodeTlvs.SERVICES.value) services = message.get_tlv(NodeTlvs.SERVICES.value)
@ -751,67 +755,54 @@ class CoreHandler(socketserver.BaseRequestHandler):
:param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle :param core.api.tlv.coreapi.CoreLinkMessage message: link message to handle
:return: link message replies :return: link message replies
""" """
node_one_id = message.get_tlv(LinkTlvs.N1_NUMBER.value) node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value)
node_two_id = message.get_tlv(LinkTlvs.N2_NUMBER.value) node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
iface1_data = InterfaceData(
interface_one = InterfaceData( id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value),
_id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value),
name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value), name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value), mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value), ip6=message.get_tlv(LinkTlvs.IFACE1_IP6.value),
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value), ip6_mask=message.get_tlv(LinkTlvs.IFACE1_IP6_MASK.value),
) )
interface_two = InterfaceData( iface2_data = InterfaceData(
_id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value), id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value), name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value), mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value), ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value),
ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value), ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value),
) )
link_type = LinkTypes.WIRED
link_type = None
link_type_value = message.get_tlv(LinkTlvs.TYPE.value) link_type_value = message.get_tlv(LinkTlvs.TYPE.value)
if link_type_value is not None: if link_type_value is not None:
link_type = LinkTypes(link_type_value) link_type = LinkTypes(link_type_value)
options = LinkOptions()
link_options = LinkOptions(_type=link_type) options.delay = message.get_tlv(LinkTlvs.DELAY.value)
link_options.delay = message.get_tlv(LinkTlvs.DELAY.value) options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value)
link_options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) options.loss = message.get_tlv(LinkTlvs.LOSS.value)
link_options.session = message.get_tlv(LinkTlvs.SESSION.value) options.dup = message.get_tlv(LinkTlvs.DUP.value)
link_options.per = message.get_tlv(LinkTlvs.PER.value) options.jitter = message.get_tlv(LinkTlvs.JITTER.value)
link_options.dup = message.get_tlv(LinkTlvs.DUP.value) options.mer = message.get_tlv(LinkTlvs.MER.value)
link_options.jitter = message.get_tlv(LinkTlvs.JITTER.value) options.burst = message.get_tlv(LinkTlvs.BURST.value)
link_options.mer = message.get_tlv(LinkTlvs.MER.value) options.mburst = message.get_tlv(LinkTlvs.MBURST.value)
link_options.burst = message.get_tlv(LinkTlvs.BURST.value) options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
link_options.mburst = message.get_tlv(LinkTlvs.MBURST.value) options.key = message.get_tlv(LinkTlvs.KEY.value)
link_options.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value)
link_options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
link_options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value)
link_options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value)
link_options.key = message.get_tlv(LinkTlvs.KEY.value)
link_options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value)
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
self.session.add_link( self.session.add_link(
node_one_id, node_two_id, interface_one, interface_two, link_options node1_id, node2_id, iface1_data, iface2_data, options, link_type
) )
elif message.flags & MessageFlags.DELETE.value: elif message.flags & MessageFlags.DELETE.value:
self.session.delete_link( self.session.delete_link(
node_one_id, node_two_id, interface_one.id, interface_two.id node1_id, node2_id, iface1_data.id, iface2_data.id, link_type
) )
else: else:
self.session.update_link( self.session.update_link(
node_one_id, node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type
node_two_id,
interface_one.id,
interface_two.id,
link_options,
) )
return () return ()
def handle_execute_message(self, message): def handle_execute_message(self, message):
@ -821,38 +812,38 @@ class CoreHandler(socketserver.BaseRequestHandler):
:param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle :param core.api.tlv.coreapi.CoreExecMessage message: execute message to handle
:return: reply messages :return: reply messages
""" """
node_num = message.get_tlv(ExecuteTlvs.NODE.value) node_id = message.get_tlv(ExecuteTlvs.NODE.value)
execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value) execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value)
execute_time = message.get_tlv(ExecuteTlvs.TIME.value) execute_time = message.get_tlv(ExecuteTlvs.TIME.value)
command = message.get_tlv(ExecuteTlvs.COMMAND.value) command = message.get_tlv(ExecuteTlvs.COMMAND.value)
# local flag indicates command executed locally, not on a node # local flag indicates command executed locally, not on a node
if node_num is None and not message.flags & MessageFlags.LOCAL.value: if node_id is None and not message.flags & MessageFlags.LOCAL.value:
raise ValueError("Execute Message is missing node number.") raise ValueError("Execute Message is missing node number.")
if execute_num is None: if execute_num is None:
raise ValueError("Execute Message is missing execution number.") raise ValueError("Execute Message is missing execution number.")
if execute_time is not None: if execute_time is not None:
self.session.add_event(execute_time, node=node_num, name=None, data=command) self.session.add_event(
float(execute_time), node_id=node_id, name=None, data=command
)
return () return ()
try: try:
node = self.session.get_node(node_num, CoreNodeBase) node = self.session.get_node(node_id, CoreNodeBase)
# build common TLV items for reply # build common TLV items for reply
tlv_data = b"" tlv_data = b""
if node_num is not None: if node_id is not None:
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.NODE.value, node_id)
ExecuteTlvs.NODE.value, node_num
)
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(
ExecuteTlvs.NUMBER.value, execute_num ExecuteTlvs.NUMBER.value, execute_num
) )
tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command) tlv_data += coreapi.CoreExecuteTlv.pack(ExecuteTlvs.COMMAND.value, command)
if message.flags & MessageFlags.TTY.value: if message.flags & MessageFlags.TTY.value:
if node_num is None: if node_id is None:
raise NotImplementedError raise NotImplementedError
# echo back exec message with cmd for spawning interactive terminal # echo back exec message with cmd for spawning interactive terminal
if command == "bash": if command == "bash":
@ -862,7 +853,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data) reply = coreapi.CoreExecMessage.pack(MessageFlags.TTY.value, tlv_data)
return (reply,) return (reply,)
else: else:
logging.info("execute message with cmd=%s", command)
# execute command and send a response # execute command and send a response
if ( if (
message.flags & MessageFlags.STRING.value message.flags & MessageFlags.STRING.value
@ -882,7 +872,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
except CoreCommandError as e: except CoreCommandError as e:
res = e.stderr res = e.stderr
status = e.returncode status = e.returncode
logging.info("done exec cmd=%s with status=%d", command, status)
if message.flags & MessageFlags.TEXT.value: if message.flags & MessageFlags.TEXT.value:
tlv_data += coreapi.CoreExecuteTlv.pack( tlv_data += coreapi.CoreExecuteTlv.pack(
ExecuteTlvs.RESULT.value, res ExecuteTlvs.RESULT.value, res
@ -900,7 +889,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
else: else:
node.cmd(command, wait=False) node.cmd(command, wait=False)
except CoreError: except CoreError:
logging.exception("error getting object: %s", node_num) logging.exception("error getting object: %s", node_id)
# XXX wait and queue this message to try again later # XXX wait and queue this message to try again later
# XXX maybe this should be done differently # XXX maybe this should be done differently
if not message.flags & MessageFlags.LOCAL.value: if not message.flags & MessageFlags.LOCAL.value:
@ -1022,7 +1011,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value), possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value),
groups=message.get_tlv(ConfigTlvs.GROUPS.value), groups=message.get_tlv(ConfigTlvs.GROUPS.value),
session=message.get_tlv(ConfigTlvs.SESSION.value), session=message.get_tlv(ConfigTlvs.SESSION.value),
interface_number=message.get_tlv(ConfigTlvs.INTERFACE_NUMBER.value), iface_id=message.get_tlv(ConfigTlvs.IFACE_ID.value),
network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value), network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value),
opaque=message.get_tlv(ConfigTlvs.OPAQUE.value), opaque=message.get_tlv(ConfigTlvs.OPAQUE.value),
) )
@ -1339,11 +1328,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1389,11 +1378,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1421,11 +1410,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
replies = [] replies = []
node_id = config_data.node node_id = config_data.node
object_name = config_data.object object_name = config_data.object
interface_id = config_data.interface_number iface_id = config_data.iface_id
values_str = config_data.data_values values_str = config_data.data_values
if interface_id is not None: if iface_id is not None:
node_id = node_id * 1000 + interface_id node_id = node_id * 1000 + iface_id
logging.debug( logging.debug(
"received configure message for %s nodenum: %s", object_name, node_id "received configure message for %s nodenum: %s", object_name, node_id
@ -1519,7 +1508,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
return () return ()
def handle_interface_message(self, message): def handle_iface_message(self, message):
""" """
Interface Message handler. Interface Message handler.
@ -1561,11 +1550,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
if event_type == EventTypes.INSTANTIATION_STATE and isinstance( if event_type == EventTypes.INSTANTIATION_STATE and isinstance(
node, WlanNode node, WlanNode
): ):
self.session.start_mobility(node_ids=(node.id,)) self.session.start_mobility(node_ids=[node.id])
return () return ()
logging.warning( logging.warning(
"dropping unhandled event message for node: %s", node_id "dropping unhandled event message for node: %s", node.name
) )
return () return ()
self.session.set_state(event_type) self.session.set_state(event_type)
@ -1623,14 +1612,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
self.session.save_xml(filename) self.session.save_xml(filename)
elif event_type == EventTypes.SCHEDULED: elif event_type == EventTypes.SCHEDULED:
etime = event_data.time etime = event_data.time
node = event_data.node node_id = event_data.node
name = event_data.name name = event_data.name
data = event_data.data data = event_data.data
if etime is None: if etime is None:
logging.warning("Event message scheduled event missing start time") logging.warning("Event message scheduled event missing start time")
return () return ()
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
self.session.add_event(float(etime), node=node, name=name, data=data) self.session.add_event(
float(etime), node_id=node_id, name=name, data=data
)
else: else:
raise NotImplementedError raise NotImplementedError
@ -1833,16 +1824,16 @@ class CoreHandler(socketserver.BaseRequestHandler):
Return API messages that describe the current session. Return API messages that describe the current session.
""" """
# find all nodes and links # find all nodes and links
links_data = [] all_links = []
with self.session._nodes_lock: with self.session.nodes_lock:
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
self.session.broadcast_node(node, MessageFlags.ADD) self.session.broadcast_node(node, MessageFlags.ADD)
node_links = node.all_link_data(flags=MessageFlags.ADD) links = node.links(flags=MessageFlags.ADD)
links_data.extend(node_links) all_links.extend(links)
for link_data in links_data: for link in all_links:
self.session.broadcast_link(link_data) self.session.broadcast_link(link)
# send mobility model info # send mobility model info
for node_id in self.session.mobility.nodes(): for node_id in self.session.mobility.nodes():
@ -1912,8 +1903,8 @@ class CoreHandler(socketserver.BaseRequestHandler):
# TODO: send location info # TODO: send location info
# send hook scripts # send hook scripts
for state in sorted(self.session._hooks.keys()): for state in sorted(self.session.hooks.keys()):
for file_name, config_data in self.session._hooks[state]: for file_name, config_data in self.session.hooks[state]:
file_data = FileData( file_data = FileData(
message_type=MessageFlags.ADD, message_type=MessageFlags.ADD,
name=str(file_name), name=str(file_name),
@ -1949,7 +1940,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
node_count = self.session.get_node_count() node_count = self.session.get_node_count()
logging.info( logging.info(
"informed GUI about %d nodes and %d links", node_count, len(links_data) "informed GUI about %d nodes and %d links", node_count, len(all_links)
) )
@ -1962,7 +1953,7 @@ class CoreUdpHandler(CoreHandler):
MessageTypes.REGISTER.value: self.handle_register_message, MessageTypes.REGISTER.value: self.handle_register_message,
MessageTypes.CONFIG.value: self.handle_config_message, MessageTypes.CONFIG.value: self.handle_config_message,
MessageTypes.FILE.value: self.handle_file_message, MessageTypes.FILE.value: self.handle_file_message,
MessageTypes.INTERFACE.value: self.handle_interface_message, MessageTypes.INTERFACE.value: self.handle_iface_message,
MessageTypes.EVENT.value: self.handle_event_message, MessageTypes.EVENT.value: self.handle_event_message,
MessageTypes.SESSION.value: self.handle_session_message, MessageTypes.SESSION.value: self.handle_session_message,
} }

View file

@ -8,45 +8,39 @@ from typing import Dict, List
from core.api.tlv import coreapi, structutils from core.api.tlv import coreapi, structutils
from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
from core.config import ConfigGroup, ConfigurableOptions from core.config import ConfigGroup, ConfigurableOptions
from core.emulator.data import ConfigData from core.emulator.data import ConfigData, NodeData
def convert_node(node_data): def convert_node(node_data: NodeData):
""" """
Convenience method for converting NodeData to a packed TLV message. Convenience method for converting NodeData to a packed TLV message.
:param core.emulator.data.NodeData node_data: node data to convert :param core.emulator.data.NodeData node_data: node data to convert
:return: packed node message :return: packed node message
""" """
session = None node = node_data.node
if node_data.session is not None:
session = str(node_data.session)
services = None services = None
if node_data.services is not None: if node.services is not None:
services = "|".join([x for x in node_data.services]) services = "|".join([x.name for x in node.services])
server = None
if node.server is not None:
server = node.server.name
tlv_data = structutils.pack_values( tlv_data = structutils.pack_values(
coreapi.CoreNodeTlv, coreapi.CoreNodeTlv,
[ [
(NodeTlvs.NUMBER, node_data.id), (NodeTlvs.NUMBER, node.id),
(NodeTlvs.TYPE, node_data.node_type.value), (NodeTlvs.TYPE, node.apitype.value),
(NodeTlvs.NAME, node_data.name), (NodeTlvs.NAME, node.name),
(NodeTlvs.IP_ADDRESS, node_data.ip_address), (NodeTlvs.MODEL, node.type),
(NodeTlvs.MAC_ADDRESS, node_data.mac_address), (NodeTlvs.EMULATION_SERVER, server),
(NodeTlvs.IP6_ADDRESS, node_data.ip6_address), (NodeTlvs.X_POSITION, int(node.position.x)),
(NodeTlvs.MODEL, node_data.model), (NodeTlvs.Y_POSITION, int(node.position.y)),
(NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.CANVAS, node.canvas),
(NodeTlvs.EMULATION_SERVER, node_data.server),
(NodeTlvs.SESSION, session),
(NodeTlvs.X_POSITION, int(node_data.x_position)),
(NodeTlvs.Y_POSITION, int(node_data.y_position)),
(NodeTlvs.CANVAS, node_data.canvas),
(NodeTlvs.NETWORK_ID, node_data.network_id),
(NodeTlvs.SERVICES, services), (NodeTlvs.SERVICES, services),
(NodeTlvs.LATITUDE, str(node_data.latitude)), (NodeTlvs.LATITUDE, str(node.position.lat)),
(NodeTlvs.LONGITUDE, str(node_data.longitude)), (NodeTlvs.LONGITUDE, str(node.position.lon)),
(NodeTlvs.ALTITUDE, str(node_data.altitude)), (NodeTlvs.ALTITUDE, str(node.position.alt)),
(NodeTlvs.ICON, node_data.icon), (NodeTlvs.ICON, node.icon),
(NodeTlvs.OPAQUE, node_data.opaque),
], ],
) )
return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data) return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
@ -75,7 +69,7 @@ def convert_config(config_data):
(ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values), (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
(ConfigTlvs.GROUPS, config_data.groups), (ConfigTlvs.GROUPS, config_data.groups),
(ConfigTlvs.SESSION, session), (ConfigTlvs.SESSION, session),
(ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number), (ConfigTlvs.IFACE_ID, config_data.iface_id),
(ConfigTlvs.NETWORK_ID, config_data.network_id), (ConfigTlvs.NETWORK_ID, config_data.network_id),
(ConfigTlvs.OPAQUE, config_data.opaque), (ConfigTlvs.OPAQUE, config_data.opaque),
], ],

View file

@ -59,7 +59,7 @@ class LinkTlvs(Enum):
N2_NUMBER = 0x02 N2_NUMBER = 0x02
DELAY = 0x03 DELAY = 0x03
BANDWIDTH = 0x04 BANDWIDTH = 0x04
PER = 0x05 LOSS = 0x05
DUP = 0x06 DUP = 0x06
JITTER = 0x07 JITTER = 0x07
MER = 0x08 MER = 0x08
@ -72,18 +72,18 @@ class LinkTlvs(Enum):
EMULATION_ID = 0x23 EMULATION_ID = 0x23
NETWORK_ID = 0x24 NETWORK_ID = 0x24
KEY = 0x25 KEY = 0x25
INTERFACE1_NUMBER = 0x30 IFACE1_NUMBER = 0x30
INTERFACE1_IP4 = 0x31 IFACE1_IP4 = 0x31
INTERFACE1_IP4_MASK = 0x32 IFACE1_IP4_MASK = 0x32
INTERFACE1_MAC = 0x33 IFACE1_MAC = 0x33
INTERFACE1_IP6 = 0x34 IFACE1_IP6 = 0x34
INTERFACE1_IP6_MASK = 0x35 IFACE1_IP6_MASK = 0x35
INTERFACE2_NUMBER = 0x36 IFACE2_NUMBER = 0x36
INTERFACE2_IP4 = 0x37 IFACE2_IP4 = 0x37
INTERFACE2_IP4_MASK = 0x38 IFACE2_IP4_MASK = 0x38
INTERFACE2_MAC = 0x39 IFACE2_MAC = 0x39
INTERFACE2_IP6 = 0x40 IFACE2_IP6 = 0x40
INTERFACE2_IP6_MASK = 0x41 IFACE2_IP6_MASK = 0x41
INTERFACE1_NAME = 0x42 INTERFACE1_NAME = 0x42
INTERFACE2_NAME = 0x43 INTERFACE2_NAME = 0x43
OPAQUE = 0x50 OPAQUE = 0x50
@ -118,7 +118,7 @@ class ConfigTlvs(Enum):
POSSIBLE_VALUES = 0x08 POSSIBLE_VALUES = 0x08
GROUPS = 0x09 GROUPS = 0x09
SESSION = 0x0A SESSION = 0x0A
INTERFACE_NUMBER = 0x0B IFACE_ID = 0x0B
NETWORK_ID = 0x24 NETWORK_ID = 0x24
OPAQUE = 0x50 OPAQUE = 0x50

View file

@ -4,7 +4,7 @@ Common support for configurable CORE objects.
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
@ -29,9 +29,9 @@ class ConfigGroup:
:param start: configurations start index for this group :param start: configurations start index for this group
:param stop: configurations stop index for this group :param stop: configurations stop index for this group
""" """
self.name = name self.name: str = name
self.start = start self.start: int = start
self.stop = stop self.stop: int = stop
class Configuration: class Configuration:
@ -56,18 +56,21 @@ class Configuration:
:param default: default value for configuration :param default: default value for configuration
:param options: list options if this is a configuration with a combobox :param options: list options if this is a configuration with a combobox
""" """
self.id = _id self.id: str = _id
self.type = _type self.type: ConfigDataTypes = _type
self.default = default self.default: str = default
if not options: if not options:
options = [] options = []
self.options = options self.options: List[str] = options
if not label: if not label:
label = _id label = _id
self.label = label self.label: str = label
def __str__(self): def __str__(self):
return f"{self.__class__.__name__}(id={self.id}, type={self.type}, default={self.default}, options={self.options})" return (
f"{self.__class__.__name__}(id={self.id}, type={self.type}, "
f"default={self.default}, options={self.options})"
)
class ConfigurableOptions: class ConfigurableOptions:
@ -75,9 +78,9 @@ class ConfigurableOptions:
Provides a base for defining configuration options within CORE. Provides a base for defining configuration options within CORE.
""" """
name = None name: Optional[str] = None
bitmap = None bitmap: Optional[str] = None
options = [] options: List[Configuration] = []
@classmethod @classmethod
def configurations(cls) -> List[Configuration]: def configurations(cls) -> List[Configuration]:
@ -115,8 +118,8 @@ class ConfigurableManager:
nodes. nodes.
""" """
_default_node = -1 _default_node: int = -1
_default_type = _default_node _default_type: int = _default_node
def __init__(self) -> None: def __init__(self) -> None:
""" """
@ -136,7 +139,8 @@ class ConfigurableManager:
""" """
Clears all configurations or configuration for a specific node. Clears all configurations or configuration for a specific node.
:param node_id: node id to clear configurations for, default is None and clears all configurations :param node_id: node id to clear configurations for, default is None and clears
all configurations
:return: nothing :return: nothing
""" """
if not node_id: if not node_id:
@ -222,7 +226,7 @@ class ConfigurableManager:
result = node_configs.get(config_type) result = node_configs.get(config_type)
return result return result
def get_all_configs(self, node_id: int = _default_node) -> List[Dict[str, str]]: def get_all_configs(self, node_id: int = _default_node) -> Dict[str, Any]:
""" """
Retrieve all current configuration types for a node. Retrieve all current configuration types for a node.
@ -242,8 +246,8 @@ class ModelManager(ConfigurableManager):
Creates a ModelManager object. Creates a ModelManager object.
""" """
super().__init__() super().__init__()
self.models = {} self.models: Dict[str, Any] = {}
self.node_models = {} self.node_models: Dict[int, str] = {}
def set_model_config( def set_model_config(
self, node_id: int, model_name: str, config: Dict[str, str] = None self, node_id: int, model_name: str, config: Dict[str, str] = None

View file

@ -14,7 +14,7 @@ from core.config import Configuration
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
TEMPLATES_DIR = "templates" TEMPLATES_DIR: str = "templates"
class ConfigServiceMode(enum.Enum): class ConfigServiceMode(enum.Enum):
@ -33,10 +33,10 @@ class ConfigService(abc.ABC):
""" """
# validation period in seconds, how frequent validation is attempted # validation period in seconds, how frequent validation is attempted
validation_period = 0.5 validation_period: float = 0.5
# time to wait in seconds for determining if service started successfully # time to wait in seconds for determining if service started successfully
validation_timer = 5 validation_timer: int = 5
def __init__(self, node: CoreNode) -> None: def __init__(self, node: CoreNode) -> None:
""" """
@ -44,13 +44,13 @@ class ConfigService(abc.ABC):
:param node: node this service is assigned to :param node: node this service is assigned to
""" """
self.node = node self.node: CoreNode = node
class_file = inspect.getfile(self.__class__) class_file = inspect.getfile(self.__class__)
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates = TemplateLookup(directories=templates_path) self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config = {} self.config: Dict[str, Configuration] = {}
self.custom_templates = {} self.custom_templates: Dict[str, str] = {}
self.custom_config = {} self.custom_config: Dict[str, str] = {}
configs = self.default_configs[:] configs = self.default_configs[:]
self._define_config(configs) self._define_config(configs)

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, List from typing import TYPE_CHECKING, Dict, List, Set
if TYPE_CHECKING: if TYPE_CHECKING:
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -17,9 +17,9 @@ class ConfigServiceDependencies:
:param services: services for determining dependency sets :param services: services for determining dependency sets
""" """
# helpers to check validity # helpers to check validity
self.dependents = {} self.dependents: Dict[str, Set[str]] = {}
self.started = set() self.started: Set[str] = set()
self.node_services = {} self.node_services: Dict[str, "ConfigService"] = {}
for service in services.values(): for service in services.values():
self.node_services[service.name] = service self.node_services[service.name] = service
for dependency in service.dependencies: for dependency in service.dependencies:
@ -27,9 +27,9 @@ class ConfigServiceDependencies:
dependents.add(service.name) dependents.add(service.name)
# used to find paths # used to find paths
self.path = [] self.path: List["ConfigService"] = []
self.visited = set() self.visited: Set[str] = set()
self.visiting = set() self.visiting: Set[str] = set()
def startup_paths(self) -> List[List["ConfigService"]]: def startup_paths(self) -> List[List["ConfigService"]]:
""" """

View file

@ -1,6 +1,6 @@
import logging import logging
import pathlib import pathlib
from typing import List, Type from typing import Dict, List, Type
from core import utils from core import utils
from core.configservice.base import ConfigService from core.configservice.base import ConfigService
@ -16,7 +16,7 @@ class ConfigServiceManager:
""" """
Create a ConfigServiceManager instance. Create a ConfigServiceManager instance.
""" """
self.services = {} self.services: Dict[str, Type[ConfigService]] = {}
def get_service(self, name: str) -> Type[ConfigService]: def get_service(self, name: str) -> Type[ConfigService]:
""" """
@ -31,7 +31,7 @@ class ConfigServiceManager:
raise CoreError(f"service does not exit {name}") raise CoreError(f"service does not exit {name}")
return service_class return service_class
def add(self, service: ConfigService) -> None: def add(self, service: Type[ConfigService]) -> None:
""" """
Add service to manager, checking service requirements have been met. Add service to manager, checking service requirements have been met.
@ -40,7 +40,9 @@ class ConfigServiceManager:
:raises CoreError: when service is a duplicate or has unmet executables :raises CoreError: when service is a duplicate or has unmet executables
""" """
name = service.name name = service.name
logging.debug("loading service: class(%s) name(%s)", service.__class__, name) logging.debug(
"loading service: class(%s) name(%s)", service.__class__.__name__, name
)
# avoid duplicate services # avoid duplicate services
if name in self.services: if name in self.services:
@ -50,10 +52,8 @@ class ConfigServiceManager:
for executable in service.executables: for executable in service.executables:
try: try:
utils.which(executable, required=True) utils.which(executable, required=True)
except ValueError: except CoreError as e:
raise CoreError( raise CoreError(f"config service({service.name}): {e}")
f"service({service.name}) missing executable {executable}"
)
# make service available # make service available
self.services[name] = service self.services[name] = service
@ -73,7 +73,6 @@ class ConfigServiceManager:
logging.debug("loading config services from: %s", subdir) logging.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService) services = utils.load_classes(str(subdir), ConfigService)
for service in services: for service in services:
logging.debug("found service: %s", service)
try: try:
self.add(service) self.add(service)
except CoreError as e: except CoreError as e:

View file

@ -1,45 +1,44 @@
import abc import abc
from typing import Any, Dict from typing import Any, Dict, List
import netaddr from core.config import Configuration
from core import constants
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNodeBase from core.nodes.base import CoreNodeBase
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
GROUP = "FRR" GROUP: str = "FRR"
FRR_STATE_DIR: str = "/var/run/frr"
def has_mtu_mismatch(ifc: CoreInterface) -> bool: def has_mtu_mismatch(iface: CoreInterface) -> bool:
""" """
Helper to detect MTU mismatch and add the appropriate FRR Helper to detect MTU mismatch and add the appropriate FRR
mtu-ignore command. This is needed when e.g. a node is linked via a mtu-ignore command. This is needed when e.g. a node is linked via a
GreTap device. GreTap device.
""" """
if ifc.mtu != 1500: if iface.mtu != 1500:
return True return True
if not ifc.net: if not iface.net:
return False return False
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu != ifc.mtu: if iface.mtu != iface.mtu:
return True return True
return False return False
def get_min_mtu(ifc): def get_min_mtu(iface: CoreInterface) -> int:
""" """
Helper to discover the minimum MTU of interfaces linked with the Helper to discover the minimum MTU of interfaces linked with the
given interface. given interface.
""" """
mtu = ifc.mtu mtu = iface.mtu
if not ifc.net: if not iface.net:
return mtu return mtu
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu < mtu: if iface.mtu < mtu:
mtu = i.mtu mtu = iface.mtu
return mtu return mtu
@ -47,34 +46,31 @@ def get_router_id(node: CoreNodeBase) -> str:
""" """
Helper to return the first IPv4 address of a node as its router ID. Helper to return the first IPv4 address of a node as its router ID.
""" """
for ifc in node.netifs(): for iface in node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
for a in ifc.addrlist: return str(ip4.ip)
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
return a
return "0.0.0.0" return "0.0.0.0"
class FRRZebra(ConfigService): class FRRZebra(ConfigService):
name = "FRRzebra" name: str = "FRRzebra"
group = GROUP group: str = GROUP
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] directories: List[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
files = [ files: List[str] = [
"/usr/local/etc/frr/frr.conf", "/usr/local/etc/frr/frr.conf",
"frrboot.sh", "frrboot.sh",
"/usr/local/etc/frr/vtysh.conf", "/usr/local/etc/frr/vtysh.conf",
"/usr/local/etc/frr/daemons", "/usr/local/etc/frr/daemons",
] ]
executables = ["zebra"] executables: List[str] = ["zebra"]
dependencies = [] dependencies: List[str] = []
startup = ["sh frrboot.sh zebra"] startup: List[str] = ["sh frrboot.sh zebra"]
validate = ["pidof zebra"] validate: List[str] = ["pidof zebra"]
shutdown = ["killall zebra"] shutdown: List[str] = ["killall zebra"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
frr_conf = self.files[0] frr_conf = self.files[0]
@ -91,31 +87,31 @@ class FRRZebra(ConfigService):
for service in self.node.config_services.values(): for service in self.node.config_services.values():
if self.name not in service.dependencies: if self.name not in service.dependencies:
continue continue
if not isinstance(service, FrrService):
continue
if service.ipv4_routing: if service.ipv4_routing:
want_ip4 = True want_ip4 = True
if service.ipv6_routing: if service.ipv6_routing:
want_ip6 = True want_ip6 = True
services.append(service) services.append(service)
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
ip4s = [] ip4s = []
ip6s = [] ip6s = []
for x in ifc.addrlist: for ip4 in iface.ip4s:
addr = x.split("/")[0] ip4s.append(str(ip4.ip))
if netaddr.valid_ipv4(addr): for ip6 in iface.ip6s:
ip4s.append(x) ip6s.append(str(ip6.ip))
else: is_control = getattr(iface, "control", False)
ip6s.append(x) ifaces.append((iface, ip4s, ip6s, is_control))
is_control = getattr(ifc, "control", False)
interfaces.append((ifc, ip4s, ip6s, is_control))
return dict( return dict(
frr_conf=frr_conf, frr_conf=frr_conf,
frr_sbin_search=frr_sbin_search, frr_sbin_search=frr_sbin_search,
frr_bin_search=frr_bin_search, frr_bin_search=frr_bin_search,
frr_state_dir=constants.FRR_STATE_DIR, frr_state_dir=FRR_STATE_DIR,
interfaces=interfaces, ifaces=ifaces,
want_ip4=want_ip4, want_ip4=want_ip4,
want_ip6=want_ip6, want_ip6=want_ip6,
services=services, services=services,
@ -123,22 +119,22 @@ class FRRZebra(ConfigService):
class FrrService(abc.ABC): class FrrService(abc.ABC):
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = [] files: List[str] = []
executables = [] executables: List[str] = []
dependencies = ["FRRzebra"] dependencies: List[str] = ["FRRzebra"]
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
ipv4_routing = False ipv4_routing: bool = False
ipv6_routing = False ipv6_routing: bool = False
@abc.abstractmethod @abc.abstractmethod
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -153,22 +149,17 @@ class FRROspfv2(FrrService, ConfigService):
unified frr.conf file. unified frr.conf file.
""" """
name = "FRROSPFv2" name: str = "FRROSPFv2"
startup = () shutdown: List[str] = ["killall ospfd"]
shutdown = ["killall ospfd"] validate: List[str] = ["pidof ospfd"]
validate = ["pidof ospfd"] ipv4_routing: bool = True
ipv4_routing = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
addresses = [] addresses = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue addresses.append(str(ip4.ip))
for a in ifc.addrlist:
addr = a.split("/")[0]
if netaddr.valid_ipv4(addr):
addresses.append(a)
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf
@ -180,8 +171,8 @@ class FRROspfv2(FrrService, ConfigService):
""" """
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(ifc): if has_mtu_mismatch(iface):
return "ip ospf mtu-ignore" return "ip ospf mtu-ignore"
else: else:
return "" return ""
@ -194,19 +185,17 @@ class FRROspfv3(FrrService, ConfigService):
unified frr.conf file. unified frr.conf file.
""" """
name = "FRROSPFv3" name: str = "FRROSPFv3"
shutdown = ["killall ospf6d"] shutdown: List[str] = ["killall ospf6d"]
validate = ["pidof ospf6d"] validate: List[str] = ["pidof ospf6d"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
data = dict(router_id=router_id, ifnames=ifnames) data = dict(router_id=router_id, ifnames=ifnames)
text = """ text = """
router ospf6 router ospf6
@ -218,9 +207,9 @@ class FRROspfv3(FrrService, ConfigService):
""" """
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
mtu = get_min_mtu(ifc) mtu = get_min_mtu(iface)
if mtu < ifc.mtu: if mtu < iface.mtu:
return f"ipv6 ospf6 ifmtu {mtu}" return f"ipv6 ospf6 ifmtu {mtu}"
else: else:
return "" return ""
@ -233,12 +222,12 @@ class FRRBgp(FrrService, ConfigService):
having the same AS number. having the same AS number.
""" """
name = "FRRBGP" name: str = "FRRBGP"
shutdown = ["killall bgpd"] shutdown: List[str] = ["killall bgpd"]
validate = ["pidof bgpd"] validate: List[str] = ["pidof bgpd"]
custom_needed = True custom_needed: bool = True
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
@ -254,7 +243,7 @@ class FRRBgp(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -263,10 +252,10 @@ class FRRRip(FrrService, ConfigService):
The RIP service provides IPv4 routing for wired networks. The RIP service provides IPv4 routing for wired networks.
""" """
name = "FRRRIP" name: str = "FRRRIP"
shutdown = ["killall ripd"] shutdown: List[str] = ["killall ripd"]
validate = ["pidof ripd"] validate: List[str] = ["pidof ripd"]
ipv4_routing = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
text = """ text = """
@ -279,7 +268,7 @@ class FRRRip(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -288,10 +277,10 @@ class FRRRipng(FrrService, ConfigService):
The RIP NG service provides IPv6 routing for wired networks. The RIP NG service provides IPv6 routing for wired networks.
""" """
name = "FRRRIPNG" name: str = "FRRRIPNG"
shutdown = ["killall ripngd"] shutdown: List[str] = ["killall ripngd"]
validate = ["pidof ripngd"] validate: List[str] = ["pidof ripngd"]
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
text = """ text = """
@ -304,7 +293,7 @@ class FRRRipng(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -314,17 +303,15 @@ class FRRBabel(FrrService, ConfigService):
protocol for IPv6 and IPv4 with fast convergence properties. protocol for IPv6 and IPv4 with fast convergence properties.
""" """
name = "FRRBabel" name: str = "FRRBabel"
shutdown = ["killall babeld"] shutdown: List[str] = ["killall babeld"]
validate = ["pidof babeld"] validate: List[str] = ["pidof babeld"]
ipv6_routing = True ipv6_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
text = """ text = """
router babel router babel
% for ifname in ifnames: % for ifname in ifnames:
@ -337,8 +324,8 @@ class FRRBabel(FrrService, ConfigService):
data = dict(ifnames=ifnames) data = dict(ifnames=ifnames)
return self.render_text(text, data) return self.render_text(text, data)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon
@ -356,16 +343,16 @@ class FRRpimd(FrrService, ConfigService):
PIM multicast routing based on XORP. PIM multicast routing based on XORP.
""" """
name = "FRRpimd" name: str = "FRRpimd"
shutdown = ["killall pimd"] shutdown: List[str] = ["killall pimd"]
validate = ["pidof pimd"] validate: List[str] = ["pidof pimd"]
ipv4_routing = True ipv4_routing: bool = True
def frr_config(self) -> str: def frr_config(self) -> str:
ifname = "eth0" ifname = "eth0"
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
if ifc.name != "lo": if iface.name != "lo":
ifname = ifc.name ifname = iface.name
break break
text = f""" text = f"""
@ -382,7 +369,7 @@ class FRRpimd(FrrService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def frr_interface_config(self, ifc: CoreInterface) -> str: def frr_iface_config(self, iface: CoreInterface) -> str:
text = """ text = """
ip mfea ip mfea
ip igmp ip igmp

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces: % for iface, ip4s, ip6s, is_control in ifaces:
interface ${ifc.name} interface ${iface.name}
% if want_ip4: % if want_ip4:
% for addr in ip4s: % for addr in ip4s:
ip address ${addr} ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif % endif
% if not is_control: % if not is_control:
% for service in services: % for service in services:
% for line in service.frr_interface_config(ifc).split("\n"): % for line in service.frr_iface_config(iface).split("\n"):
${line} ${line}
% endfor % endfor
% endfor % endfor

View file

@ -98,8 +98,8 @@ confcheck
bootfrr bootfrr
# reset interfaces # reset interfaces
% for ifc, _, _ , _ in interfaces: % for iface, _, _ , _ in ifaces:
ip link set dev ${ifc.name} down ip link set dev ${iface.name} down
sleep 1 sleep 1
ip link set dev ${ifc.name} up ip link set dev ${iface.name} up
% endfor % endfor

View file

@ -1,72 +1,69 @@
from typing import Any, Dict from typing import Any, Dict, List
import netaddr
from core import utils from core import utils
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
GROUP = "ProtoSvc" GROUP: str = "ProtoSvc"
class MgenSinkService(ConfigService): class MgenSinkService(ConfigService):
name = "MGEN_Sink" name: str = "MGEN_Sink"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["mgensink.sh", "sink.mgen"] files: List[str] = ["mgensink.sh", "sink.mgen"]
executables = ["mgen"] executables: List[str] = ["mgen"]
dependencies = [] dependencies: List[str] = []
startup = ["sh mgensink.sh"] startup: List[str] = ["sh mgensink.sh"]
validate = ["pidof mgen"] validate: List[str] = ["pidof mgen"]
shutdown = ["killall mgen"] shutdown: List[str] = ["killall mgen"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
name = utils.sysctl_devname(ifc.name) name = utils.sysctl_devname(iface.name)
ifnames.append(name) ifnames.append(name)
return dict(ifnames=ifnames) return dict(ifnames=ifnames)
class NrlNhdp(ConfigService): class NrlNhdp(ConfigService):
name = "NHDP" name: str = "NHDP"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlnhdp.sh"] files: List[str] = ["nrlnhdp.sh"]
executables = ["nrlnhdp"] executables: List[str] = ["nrlnhdp"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlnhdp.sh"] startup: List[str] = ["sh nrlnhdp.sh"]
validate = ["pidof nrlnhdp"] validate: List[str] = ["pidof nrlnhdp"]
shutdown = ["killall nrlnhdp"] shutdown: List[str] = ["killall nrlnhdp"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class NrlSmf(ConfigService): class NrlSmf(ConfigService):
name = "SMF" name: str = "SMF"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["startsmf.sh"] files: List[str] = ["startsmf.sh"]
executables = ["nrlsmf", "killall"] executables: List[str] = ["nrlsmf", "killall"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startsmf.sh"] startup: List[str] = ["sh startsmf.sh"]
validate = ["pidof nrlsmf"] validate: List[str] = ["pidof nrlsmf"]
shutdown = ["killall nrlsmf"] shutdown: List[str] = ["killall nrlsmf"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_arouted = "arouted" in self.node.config_services has_arouted = "arouted" in self.node.config_services
@ -74,16 +71,11 @@ class NrlSmf(ConfigService):
has_olsr = "OLSR" in self.node.config_services has_olsr = "OLSR" in self.node.config_services
ifnames = [] ifnames = []
ip4_prefix = None ip4_prefix = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue ip4 = iface.get_ip4()
ifnames.append(ifc.name) if ip4:
if ip4_prefix: ip4_prefix = f"{ip4.ip}/{24}"
continue
for a in ifc.addrlist:
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
ip4_prefix = f"{a}/{24}"
break break
return dict( return dict(
has_arouted=has_arouted, has_arouted=has_arouted,
@ -95,118 +87,107 @@ class NrlSmf(ConfigService):
class NrlOlsr(ConfigService): class NrlOlsr(ConfigService):
name = "OLSR" name: str = "OLSR"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlolsrd.sh"] files: List[str] = ["nrlolsrd.sh"]
executables = ["nrlolsrd"] executables: List[str] = ["nrlolsrd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlolsrd.sh"] startup: List[str] = ["sh nrlolsrd.sh"]
validate = ["pidof nrlolsrd"] validate: List[str] = ["pidof nrlolsrd"]
shutdown = ["killall nrlolsrd"] shutdown: List[str] = ["killall nrlolsrd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
has_zebra = "zebra" in self.node.config_services has_zebra = "zebra" in self.node.config_services
ifname = None ifname = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifname = iface.name
continue
ifname = ifc.name
break break
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname) return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
class NrlOlsrv2(ConfigService): class NrlOlsrv2(ConfigService):
name = "OLSRv2" name: str = "OLSRv2"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["nrlolsrv2.sh"] files: List[str] = ["nrlolsrv2.sh"]
executables = ["nrlolsrv2"] executables: List[str] = ["nrlolsrv2"]
dependencies = [] dependencies: List[str] = []
startup = ["sh nrlolsrv2.sh"] startup: List[str] = ["sh nrlolsrv2.sh"]
validate = ["pidof nrlolsrv2"] validate: List[str] = ["pidof nrlolsrv2"]
shutdown = ["killall nrlolsrv2"] shutdown: List[str] = ["killall nrlolsrv2"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class OlsrOrg(ConfigService): class OlsrOrg(ConfigService):
name = "OLSRORG" name: str = "OLSRORG"
group = GROUP group: str = GROUP
directories = ["/etc/olsrd"] directories: List[str] = ["/etc/olsrd"]
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"] files: List[str] = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
executables = ["olsrd"] executables: List[str] = ["olsrd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh olsrd.sh"] startup: List[str] = ["sh olsrd.sh"]
validate = ["pidof olsrd"] validate: List[str] = ["pidof olsrd"]
shutdown = ["killall olsrd"] shutdown: List[str] = ["killall olsrd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
has_smf = "SMF" in self.node.config_services has_smf = "SMF" in self.node.config_services
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(has_smf=has_smf, ifnames=ifnames) return dict(has_smf=has_smf, ifnames=ifnames)
class MgenActor(ConfigService): class MgenActor(ConfigService):
name = "MgenActor" name: str = "MgenActor"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["start_mgen_actor.sh"] files: List[str] = ["start_mgen_actor.sh"]
executables = ["mgen"] executables: List[str] = ["mgen"]
dependencies = [] dependencies: List[str] = []
startup = ["sh start_mgen_actor.sh"] startup: List[str] = ["sh start_mgen_actor.sh"]
validate = ["pidof mgen"] validate: List[str] = ["pidof mgen"]
shutdown = ["killall mgen"] shutdown: List[str] = ["killall mgen"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class Arouted(ConfigService): class Arouted(ConfigService):
name = "arouted" name: str = "arouted"
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = ["startarouted.sh"] files: List[str] = ["startarouted.sh"]
executables = ["arouted"] executables: List[str] = ["arouted"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startarouted.sh"] startup: List[str] = ["sh startarouted.sh"]
validate = ["pidof arouted"] validate: List[str] = ["pidof arouted"]
shutdown = ["pkill arouted"] shutdown: List[str] = ["pkill arouted"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ip4_prefix = None ip4_prefix = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
if ip4_prefix: ip4_prefix = f"{ip4.ip}/{24}"
continue
for a in ifc.addrlist:
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
ip4_prefix = f"{a}/{24}"
break break
return dict(ip4_prefix=ip4_prefix) return dict(ip4_prefix=ip4_prefix)

View file

@ -1,7 +1,7 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
smf = "" smf = ""
if has_smf: if has_smf:
smf = "-flooding ecds -smfClient %s_smf" % node.name smf = "-flooding ecds -smfClient %s_smf" % node.name
%> %>
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces} nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces}

View file

@ -1,7 +1,7 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
smf = "" smf = ""
if has_smf: if has_smf:
smf = "-flooding ecds -smfClient %s_smf" % node.name smf = "-flooding ecds -smfClient %s_smf" % node.name
%> %>
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces} nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces}

View file

@ -1,4 +1,4 @@
<% <%
interfaces = "-i " + " -i ".join(ifnames) ifaces = "-i " + " -i ".join(ifnames)
%> %>
olsrd ${interfaces} olsrd ${ifaces}

View file

@ -1,5 +1,5 @@
<% <%
interfaces = ",".join(ifnames) ifaces = ",".join(ifnames)
arouted = "" arouted = ""
if has_arouted: if has_arouted:
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0]) arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
@ -12,4 +12,4 @@
%> %>
#!/bin/sh #!/bin/sh
# auto-generated by NrlSmf service # auto-generated by NrlSmf service
nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 & nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &

View file

@ -1,46 +1,45 @@
import abc import abc
import logging import logging
from typing import Any, Dict from typing import Any, Dict, List
import netaddr from core.config import Configuration
from core import constants
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.nodes.base import CoreNodeBase from core.nodes.base import CoreNodeBase
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
GROUP = "Quagga" GROUP: str = "Quagga"
QUAGGA_STATE_DIR: str = "/var/run/quagga"
def has_mtu_mismatch(ifc: CoreInterface) -> bool: def has_mtu_mismatch(iface: CoreInterface) -> bool:
""" """
Helper to detect MTU mismatch and add the appropriate OSPF Helper to detect MTU mismatch and add the appropriate OSPF
mtu-ignore command. This is needed when e.g. a node is linked via a mtu-ignore command. This is needed when e.g. a node is linked via a
GreTap device. GreTap device.
""" """
if ifc.mtu != 1500: if iface.mtu != 1500:
return True return True
if not ifc.net: if not iface.net:
return False return False
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu != ifc.mtu: if iface.mtu != iface.mtu:
return True return True
return False return False
def get_min_mtu(ifc): def get_min_mtu(iface: CoreInterface):
""" """
Helper to discover the minimum MTU of interfaces linked with the Helper to discover the minimum MTU of interfaces linked with the
given interface. given interface.
""" """
mtu = ifc.mtu mtu = iface.mtu
if not ifc.net: if not iface.net:
return mtu return mtu
for i in ifc.net.netifs(): for iface in iface.net.get_ifaces():
if i.mtu < mtu: if iface.mtu < mtu:
mtu = i.mtu mtu = iface.mtu
return mtu return mtu
@ -48,33 +47,30 @@ def get_router_id(node: CoreNodeBase) -> str:
""" """
Helper to return the first IPv4 address of a node as its router ID. Helper to return the first IPv4 address of a node as its router ID.
""" """
for ifc in node.netifs(): for iface in node.get_ifaces(control=False):
if getattr(ifc, "control", False): ip4 = iface.get_ip4()
continue if ip4:
for a in ifc.addrlist: return str(ip4.ip)
a = a.split("/")[0]
if netaddr.valid_ipv4(a):
return a
return "0.0.0.0" return "0.0.0.0"
class Zebra(ConfigService): class Zebra(ConfigService):
name = "zebra" name: str = "zebra"
group = GROUP group: str = GROUP
directories = ["/usr/local/etc/quagga", "/var/run/quagga"] directories: List[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
files = [ files: List[str] = [
"/usr/local/etc/quagga/Quagga.conf", "/usr/local/etc/quagga/Quagga.conf",
"quaggaboot.sh", "quaggaboot.sh",
"/usr/local/etc/quagga/vtysh.conf", "/usr/local/etc/quagga/vtysh.conf",
] ]
executables = ["zebra"] executables: List[str] = ["zebra"]
dependencies = [] dependencies: List[str] = []
startup = ["sh quaggaboot.sh zebra"] startup: List[str] = ["sh quaggaboot.sh zebra"]
validate = ["pidof zebra"] validate: List[str] = ["pidof zebra"]
shutdown = ["killall zebra"] shutdown: List[str] = ["killall zebra"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
quagga_bin_search = self.node.session.options.get_config( quagga_bin_search = self.node.session.options.get_config(
@ -83,7 +79,7 @@ class Zebra(ConfigService):
quagga_sbin_search = self.node.session.options.get_config( quagga_sbin_search = self.node.session.options.get_config(
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
).strip('"') ).strip('"')
quagga_state_dir = constants.QUAGGA_STATE_DIR quagga_state_dir = QUAGGA_STATE_DIR
quagga_conf = self.files[0] quagga_conf = self.files[0]
services = [] services = []
@ -92,31 +88,31 @@ class Zebra(ConfigService):
for service in self.node.config_services.values(): for service in self.node.config_services.values():
if self.name not in service.dependencies: if self.name not in service.dependencies:
continue continue
if not isinstance(service, QuaggaService):
continue
if service.ipv4_routing: if service.ipv4_routing:
want_ip4 = True want_ip4 = True
if service.ipv6_routing: if service.ipv6_routing:
want_ip6 = True want_ip6 = True
services.append(service) services.append(service)
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
ip4s = [] ip4s = []
ip6s = [] ip6s = []
for x in ifc.addrlist: for ip4 in iface.ip4s:
addr = x.split("/")[0] ip4s.append(str(ip4.ip))
if netaddr.valid_ipv4(addr): for ip6 in iface.ip6s:
ip4s.append(x) ip6s.append(str(ip6.ip))
else: is_control = getattr(iface, "control", False)
ip6s.append(x) ifaces.append((iface, ip4s, ip6s, is_control))
is_control = getattr(ifc, "control", False)
interfaces.append((ifc, ip4s, ip6s, is_control))
return dict( return dict(
quagga_bin_search=quagga_bin_search, quagga_bin_search=quagga_bin_search,
quagga_sbin_search=quagga_sbin_search, quagga_sbin_search=quagga_sbin_search,
quagga_state_dir=quagga_state_dir, quagga_state_dir=quagga_state_dir,
quagga_conf=quagga_conf, quagga_conf=quagga_conf,
interfaces=interfaces, ifaces=ifaces,
want_ip4=want_ip4, want_ip4=want_ip4,
want_ip6=want_ip6, want_ip6=want_ip6,
services=services, services=services,
@ -124,22 +120,22 @@ class Zebra(ConfigService):
class QuaggaService(abc.ABC): class QuaggaService(abc.ABC):
group = GROUP group: str = GROUP
directories = [] directories: List[str] = []
files = [] files: List[str] = []
executables = [] executables: List[str] = []
dependencies = ["zebra"] dependencies: List[str] = ["zebra"]
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
ipv4_routing = False ipv4_routing: bool = False
ipv6_routing = False ipv6_routing: bool = False
@abc.abstractmethod @abc.abstractmethod
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -154,13 +150,13 @@ class Ospfv2(QuaggaService, ConfigService):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv2" name: str = "OSPFv2"
validate = ["pidof ospfd"] validate: List[str] = ["pidof ospfd"]
shutdown = ["killall ospfd"] shutdown: List[str] = ["killall ospfd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if has_mtu_mismatch(ifc): if has_mtu_mismatch(iface):
return "ip ospf mtu-ignore" return "ip ospf mtu-ignore"
else: else:
return "" return ""
@ -168,13 +164,9 @@ class Ospfv2(QuaggaService, ConfigService):
def quagga_config(self) -> str: def quagga_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
addresses = [] addresses = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue addresses.append(str(ip4.ip))
for a in ifc.addrlist:
addr = a.split("/")[0]
if netaddr.valid_ipv4(addr):
addresses.append(a)
data = dict(router_id=router_id, addresses=addresses) data = dict(router_id=router_id, addresses=addresses)
text = """ text = """
router ospf router ospf
@ -194,15 +186,15 @@ class Ospfv3(QuaggaService, ConfigService):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv3" name: str = "OSPFv3"
shutdown = ("killall ospf6d",) shutdown: List[str] = ["killall ospf6d"]
validate = ("pidof ospf6d",) validate: List[str] = ["pidof ospf6d"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
mtu = get_min_mtu(ifc) mtu = get_min_mtu(iface)
if mtu < ifc.mtu: if mtu < iface.mtu:
return f"ipv6 ospf6 ifmtu {mtu}" return f"ipv6 ospf6 ifmtu {mtu}"
else: else:
return "" return ""
@ -210,10 +202,8 @@ class Ospfv3(QuaggaService, ConfigService):
def quagga_config(self) -> str: def quagga_config(self) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
data = dict(router_id=router_id, ifnames=ifnames) data = dict(router_id=router_id, ifnames=ifnames)
text = """ text = """
router ospf6 router ospf6
@ -235,17 +225,17 @@ class Ospfv3mdr(Ospfv3):
unified Quagga.conf file. unified Quagga.conf file.
""" """
name = "OSPFv3MDR" name: str = "OSPFv3MDR"
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet)) is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
logging.info("MDR wireless: %s", is_wireless) logging.info("MDR wireless: %s", is_wireless)
return dict() return dict()
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
config = super().quagga_interface_config(ifc) config = super().quagga_iface_config(iface)
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
config = self.clean_text( config = self.clean_text(
f""" f"""
{config} {config}
@ -268,16 +258,16 @@ class Bgp(QuaggaService, ConfigService):
having the same AS number. having the same AS number.
""" """
name = "BGP" name: str = "BGP"
shutdown = ["killall bgpd"] shutdown: List[str] = ["killall bgpd"]
validate = ["pidof bgpd"] validate: List[str] = ["pidof bgpd"]
ipv4_routing = True ipv4_routing: bool = True
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
return "" return ""
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
router_id = get_router_id(self.node) router_id = get_router_id(self.node)
text = f""" text = f"""
! BGP configuration ! BGP configuration
@ -297,10 +287,10 @@ class Rip(QuaggaService, ConfigService):
The RIP service provides IPv4 routing for wired networks. The RIP service provides IPv4 routing for wired networks.
""" """
name = "RIP" name: str = "RIP"
shutdown = ["killall ripd"] shutdown: List[str] = ["killall ripd"]
validate = ["pidof ripd"] validate: List[str] = ["pidof ripd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
text = """ text = """
@ -313,7 +303,7 @@ class Rip(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -322,10 +312,10 @@ class Ripng(QuaggaService, ConfigService):
The RIP NG service provides IPv6 routing for wired networks. The RIP NG service provides IPv6 routing for wired networks.
""" """
name = "RIPNG" name: str = "RIPNG"
shutdown = ["killall ripngd"] shutdown: List[str] = ["killall ripngd"]
validate = ["pidof ripngd"] validate: List[str] = ["pidof ripngd"]
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
text = """ text = """
@ -338,7 +328,7 @@ class Ripng(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
return "" return ""
@ -348,17 +338,15 @@ class Babel(QuaggaService, ConfigService):
protocol for IPv6 and IPv4 with fast convergence properties. protocol for IPv6 and IPv4 with fast convergence properties.
""" """
name = "Babel" name: str = "Babel"
shutdown = ["killall babeld"] shutdown: List[str] = ["killall babeld"]
validate = ["pidof babeld"] validate: List[str] = ["pidof babeld"]
ipv6_routing = True ipv6_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
text = """ text = """
router babel router babel
% for ifname in ifnames: % for ifname in ifnames:
@ -371,8 +359,8 @@ class Babel(QuaggaService, ConfigService):
data = dict(ifnames=ifnames) data = dict(ifnames=ifnames)
return self.render_text(text, data) return self.render_text(text, data)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
if isinstance(ifc.net, (WlanNode, EmaneNet)): if isinstance(iface.net, (WlanNode, EmaneNet)):
text = """ text = """
babel wireless babel wireless
no babel split-horizon no babel split-horizon
@ -390,16 +378,16 @@ class Xpimd(QuaggaService, ConfigService):
PIM multicast routing based on XORP. PIM multicast routing based on XORP.
""" """
name = "Xpimd" name: str = "Xpimd"
shutdown = ["killall xpimd"] shutdown: List[str] = ["killall xpimd"]
validate = ["pidof xpimd"] validate: List[str] = ["pidof xpimd"]
ipv4_routing = True ipv4_routing: bool = True
def quagga_config(self) -> str: def quagga_config(self) -> str:
ifname = "eth0" ifname = "eth0"
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
if ifc.name != "lo": if iface.name != "lo":
ifname = ifc.name ifname = iface.name
break break
text = f""" text = f"""
@ -416,7 +404,7 @@ class Xpimd(QuaggaService, ConfigService):
""" """
return self.clean_text(text) return self.clean_text(text)
def quagga_interface_config(self, ifc: CoreInterface) -> str: def quagga_iface_config(self, iface: CoreInterface) -> str:
text = """ text = """
ip mfea ip mfea
ip pim ip pim

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces: % for iface, ip4s, ip6s, is_control in ifaces:
interface ${ifc.name} interface ${iface.name}
% if want_ip4: % if want_ip4:
% for addr in ip4s: % for addr in ip4s:
ip address ${addr} ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif % endif
% if not is_control: % if not is_control:
% for service in services: % for service in services:
% for line in service.quagga_interface_config(ifc).split("\n"): % for line in service.quagga_iface_config(iface).split("\n"):
${line} ${line}
% endfor % endfor
% endfor % endfor

View file

@ -0,0 +1,135 @@
from typing import Any, Dict, List
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
GROUP_NAME: str = "Security"
class VpnClient(ConfigService):
name: str = "VPNClient"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["vpnclient.sh"]
executables: List[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh vpnclient.sh"]
validate: List[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="client1",
),
Configuration(
_id="server",
_type=ConfigDataTypes.STRING,
label="Server",
default="10.0.2.10",
),
]
modes: Dict[str, Dict[str, str]] = {}
class VpnServer(ConfigService):
name: str = "VPNServer"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["vpnserver.sh"]
executables: List[str] = ["openvpn", "ip", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh vpnserver.sh"]
validate: List[str] = ["pidof openvpn"]
shutdown: List[str] = ["killall openvpn"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="server",
),
Configuration(
_id="subnet",
_type=ConfigDataTypes.STRING,
label="Subnet",
default="10.0.200.0",
),
]
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]:
address = None
for iface in self.node.get_ifaces(control=False):
ip4 = iface.get_ip4()
if ip4:
address = str(ip4.ip)
break
return dict(address=address)
class IPsec(ConfigService):
name: str = "IPsec"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["ipsec.sh"]
executables: List[str] = ["racoon", "ip", "setkey", "killall"]
dependencies: List[str] = []
startup: List[str] = ["sh ipsec.sh"]
validate: List[str] = ["pidof racoon"]
shutdown: List[str] = ["killall racoon"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
class Firewall(ConfigService):
name: str = "Firewall"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["firewall.sh"]
executables: List[str] = ["iptables"]
dependencies: List[str] = []
startup: List[str] = ["sh firewall.sh"]
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
class Nat(ConfigService):
name: str = "NAT"
group: str = GROUP_NAME
directories: List[str] = []
files: List[str] = ["nat.sh"]
executables: List[str] = ["iptables"]
dependencies: List[str] = []
startup: List[str] = ["sh nat.sh"]
validate: List[str] = []
shutdown: List[str] = []
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]:
ifnames = []
for iface in self.node.get_ifaces(control=False):
ifnames.append(iface.name)
return dict(ifnames=ifnames)

View file

@ -1,141 +0,0 @@
from typing import Any, Dict
import netaddr
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes
GROUP_NAME = "Security"
class VpnClient(ConfigService):
name = "VPNClient"
group = GROUP_NAME
directories = []
files = ["vpnclient.sh"]
executables = ["openvpn", "ip", "killall"]
dependencies = []
startup = ["sh vpnclient.sh"]
validate = ["pidof openvpn"]
shutdown = ["killall openvpn"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="client1",
),
Configuration(
_id="server",
_type=ConfigDataTypes.STRING,
label="Server",
default="10.0.2.10",
),
]
modes = {}
class VpnServer(ConfigService):
name = "VPNServer"
group = GROUP_NAME
directories = []
files = ["vpnserver.sh"]
executables = ["openvpn", "ip", "killall"]
dependencies = []
startup = ["sh vpnserver.sh"]
validate = ["pidof openvpn"]
shutdown = ["killall openvpn"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = [
Configuration(
_id="keydir",
_type=ConfigDataTypes.STRING,
label="Key Dir",
default="/etc/core/keys",
),
Configuration(
_id="keyname",
_type=ConfigDataTypes.STRING,
label="Key Name",
default="server",
),
Configuration(
_id="subnet",
_type=ConfigDataTypes.STRING,
label="Subnet",
default="10.0.200.0",
),
]
modes = {}
def data(self) -> Dict[str, Any]:
address = None
for ifc in self.node.netifs():
if getattr(ifc, "control", False):
continue
for x in ifc.addrlist:
addr = x.split("/")[0]
if netaddr.valid_ipv4(addr):
address = addr
return dict(address=address)
class IPsec(ConfigService):
name = "IPsec"
group = GROUP_NAME
directories = []
files = ["ipsec.sh"]
executables = ["racoon", "ip", "setkey", "killall"]
dependencies = []
startup = ["sh ipsec.sh"]
validate = ["pidof racoon"]
shutdown = ["killall racoon"]
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
class Firewall(ConfigService):
name = "Firewall"
group = GROUP_NAME
directories = []
files = ["firewall.sh"]
executables = ["iptables"]
dependencies = []
startup = ["sh firewall.sh"]
validate = []
shutdown = []
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
class Nat(ConfigService):
name = "NAT"
group = GROUP_NAME
directories = []
files = ["nat.sh"]
executables = ["iptables"]
dependencies = []
startup = ["sh nat.sh"]
validate = []
shutdown = []
validation_mode = ConfigServiceMode.BLOCKING
default_configs = []
modes = {}
def data(self) -> Dict[str, Any]:
ifnames = []
for ifc in self.node.netifs():
if getattr(ifc, "control", False):
continue
ifnames.append(ifc.name)
return dict(ifnames=ifnames)

View file

@ -1,20 +1,22 @@
from typing import Dict, List
from core.config import Configuration from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
from core.emulator.enumerations import ConfigDataTypes from core.emulator.enumerations import ConfigDataTypes
class SimpleService(ConfigService): class SimpleService(ConfigService):
name = "Simple" name: str = "Simple"
group = "SimpleGroup" group: str = "SimpleGroup"
directories = ["/etc/quagga", "/usr/local/lib"] directories: List[str] = ["/etc/quagga", "/usr/local/lib"]
files = ["test1.sh", "test2.sh"] files: List[str] = ["test1.sh", "test2.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = [] startup: List[str] = []
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [ default_configs: List[Configuration] = [
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
Configuration( Configuration(
@ -24,7 +26,7 @@ class SimpleService(ConfigService):
options=["value1", "value2", "value3"], options=["value1", "value2", "value3"],
), ),
] ]
modes = { modes: Dict[str, Dict[str, str]] = {
"mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, "mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
"mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, "mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
"mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, "mode3": {"value1": "value3", "value2": "0", "value3": "value1"},

View file

@ -1,35 +1,36 @@
from typing import Any, Dict from typing import Any, Dict, List
import netaddr import netaddr
from core import utils from core import utils
from core.config import Configuration
from core.configservice.base import ConfigService, ConfigServiceMode from core.configservice.base import ConfigService, ConfigServiceMode
GROUP_NAME = "Utility" GROUP_NAME = "Utility"
class DefaultRouteService(ConfigService): class DefaultRouteService(ConfigService):
name = "DefaultRoute" name: str = "DefaultRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["defaultroute.sh"] files: List[str] = ["defaultroute.sh"]
executables = ["ip"] executables: List[str] = ["ip"]
dependencies = [] dependencies: List[str] = []
startup = ["sh defaultroute.sh"] startup: List[str] = ["sh defaultroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
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 = []
netifs = self.node.netifs(sort=True) ifaces = self.node.get_ifaces()
if netifs: if ifaces:
netif = netifs[0] iface = ifaces[0]
for x in netif.addrlist: for ip in iface.ips():
net = netaddr.IPNetwork(x).cidr net = ip.cidr
if net.size > 1: if net.size > 1:
router = net[1] router = net[1]
routes.append(str(router)) routes.append(str(router))
@ -37,95 +38,90 @@ class DefaultRouteService(ConfigService):
class DefaultMulticastRouteService(ConfigService): class DefaultMulticastRouteService(ConfigService):
name = "DefaultMulticastRoute" name: str = "DefaultMulticastRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["defaultmroute.sh"] files: List[str] = ["defaultmroute.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = ["sh defaultmroute.sh"] startup: List[str] = ["sh defaultmroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifname = None ifname = None
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifname = iface.name
continue
ifname = ifc.name
break break
return dict(ifname=ifname) return dict(ifname=ifname)
class StaticRouteService(ConfigService): class StaticRouteService(ConfigService):
name = "StaticRoute" name: str = "StaticRoute"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["staticroute.sh"] files: List[str] = ["staticroute.sh"]
executables = [] executables: List[str] = []
dependencies = [] dependencies: List[str] = []
startup = ["sh staticroute.sh"] startup: List[str] = ["sh staticroute.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
routes = [] routes = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip in iface.ips():
continue address = str(ip.ip)
for x in ifc.addrlist: if netaddr.valid_ipv6(address):
addr = x.split("/")[0]
if netaddr.valid_ipv6(addr):
dst = "3ffe:4::/64" dst = "3ffe:4::/64"
else: else:
dst = "10.9.8.0/24" dst = "10.9.8.0/24"
net = netaddr.IPNetwork(x) if ip[-2] != ip[1]:
if net[-2] != net[1]: routes.append((dst, ip[1]))
routes.append((dst, net[1]))
return dict(routes=routes) return dict(routes=routes)
class IpForwardService(ConfigService): class IpForwardService(ConfigService):
name = "IPForward" name: str = "IPForward"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["ipforward.sh"] files: List[str] = ["ipforward.sh"]
executables = ["sysctl"] executables: List[str] = ["sysctl"]
dependencies = [] dependencies: List[str] = []
startup = ["sh ipforward.sh"] startup: List[str] = ["sh ipforward.sh"]
validate = [] validate: List[str] = []
shutdown = [] shutdown: List[str] = []
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
devnames = [] devnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces():
devname = utils.sysctl_devname(ifc.name) devname = utils.sysctl_devname(iface.name)
devnames.append(devname) devnames.append(devname)
return dict(devnames=devnames) return dict(devnames=devnames)
class SshService(ConfigService): class SshService(ConfigService):
name = "SSH" name: str = "SSH"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/ssh", "/var/run/sshd"] directories: List[str] = ["/etc/ssh", "/var/run/sshd"]
files = ["startsshd.sh", "/etc/ssh/sshd_config"] files: List[str] = ["startsshd.sh", "/etc/ssh/sshd_config"]
executables = ["sshd"] executables: List[str] = ["sshd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startsshd.sh"] startup: List[str] = ["sh startsshd.sh"]
validate = [] validate: List[str] = []
shutdown = ["killall sshd"] shutdown: List[str] = ["killall sshd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
return dict( return dict(
@ -136,146 +132,135 @@ class SshService(ConfigService):
class DhcpService(ConfigService): class DhcpService(ConfigService):
name = "DHCP" name: str = "DHCP"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/dhcp", "/var/lib/dhcp"] directories: List[str] = ["/etc/dhcp", "/var/lib/dhcp"]
files = ["/etc/dhcp/dhcpd.conf"] files: List[str] = ["/etc/dhcp/dhcpd.conf"]
executables = ["dhcpd"] executables: List[str] = ["dhcpd"]
dependencies = [] dependencies: List[str] = []
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"] startup: List[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
validate = ["pidof dhcpd"] validate: List[str] = ["pidof dhcpd"]
shutdown = ["killall dhcpd"] shutdown: List[str] = ["killall dhcpd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
subnets = [] subnets = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): for ip4 in iface.ip4s:
continue
for x in ifc.addrlist:
addr = x.split("/")[0]
if netaddr.valid_ipv4(addr):
net = netaddr.IPNetwork(x)
# divide the address space in half # divide the address space in half
index = (net.size - 2) / 2 index = (ip4.size - 2) / 2
rangelow = net[index] rangelow = ip4[index]
rangehigh = net[-2] rangehigh = ip4[-2]
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr)) subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip)))
return dict(subnets=subnets) return dict(subnets=subnets)
class DhcpClientService(ConfigService): class DhcpClientService(ConfigService):
name = "DHCPClient" name: str = "DHCPClient"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["startdhcpclient.sh"] files: List[str] = ["startdhcpclient.sh"]
executables = ["dhclient"] executables: List[str] = ["dhclient"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startdhcpclient.sh"] startup: List[str] = ["sh startdhcpclient.sh"]
validate = ["pidof dhclient"] validate: List[str] = ["pidof dhclient"]
shutdown = ["killall dhclient"] shutdown: List[str] = ["killall dhclient"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict(ifnames=ifnames) return dict(ifnames=ifnames)
class FtpService(ConfigService): class FtpService(ConfigService):
name = "FTP" name: str = "FTP"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/var/run/vsftpd/empty", "/var/ftp"] directories: List[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
files = ["vsftpd.conf"] files: List[str] = ["vsftpd.conf"]
executables = ["vsftpd"] executables: List[str] = ["vsftpd"]
dependencies = [] dependencies: List[str] = []
startup = ["vsftpd ./vsftpd.conf"] startup: List[str] = ["vsftpd ./vsftpd.conf"]
validate = ["pidof vsftpd"] validate: List[str] = ["pidof vsftpd"]
shutdown = ["killall vsftpd"] shutdown: List[str] = ["killall vsftpd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class PcapService(ConfigService): class PcapService(ConfigService):
name = "pcap" name: str = "pcap"
group = GROUP_NAME group: str = GROUP_NAME
directories = [] directories: List[str] = []
files = ["pcap.sh"] files: List[str] = ["pcap.sh"]
executables = ["tcpdump"] executables: List[str] = ["tcpdump"]
dependencies = [] dependencies: List[str] = []
startup = ["sh pcap.sh start"] startup: List[str] = ["sh pcap.sh start"]
validate = ["pidof tcpdump"] validate: List[str] = ["pidof tcpdump"]
shutdown = ["sh pcap.sh stop"] shutdown: List[str] = ["sh pcap.sh stop"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
ifnames = [] ifnames = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifnames.append(iface.name)
continue
ifnames.append(ifc.name)
return dict() return dict()
class RadvdService(ConfigService): class RadvdService(ConfigService):
name = "radvd" name: str = "radvd"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/etc/radvd"] directories: List[str] = ["/etc/radvd"]
files = ["/etc/radvd/radvd.conf"] files: List[str] = ["/etc/radvd/radvd.conf"]
executables = ["radvd"] executables: List[str] = ["radvd"]
dependencies = [] dependencies: List[str] = []
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"] startup: List[str] = [
validate = ["pidof radvd"] "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"
shutdown = ["pkill radvd"] ]
validation_mode = ConfigServiceMode.BLOCKING validate: List[str] = ["pidof radvd"]
default_configs = [] shutdown: List[str] = ["pkill radvd"]
modes = {} validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False):
continue
prefixes = [] prefixes = []
for x in ifc.addrlist: for ip6 in iface.ip6s:
addr = x.split("/")[0] prefixes.append(str(ip6))
if netaddr.valid_ipv6(addr):
prefixes.append(x)
if not prefixes: if not prefixes:
continue continue
interfaces.append((ifc.name, prefixes)) ifaces.append((iface.name, prefixes))
return dict(interfaces=interfaces) return dict(ifaces=ifaces)
class AtdService(ConfigService): class AtdService(ConfigService):
name = "atd" name: str = "atd"
group = GROUP_NAME group: str = GROUP_NAME
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"] directories: List[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
files = ["startatd.sh"] files: List[str] = ["startatd.sh"]
executables = ["atd"] executables: List[str] = ["atd"]
dependencies = [] dependencies: List[str] = []
startup = ["sh startatd.sh"] startup: List[str] = ["sh startatd.sh"]
validate = ["pidof atd"] validate: List[str] = ["pidof atd"]
shutdown = ["pkill atd"] shutdown: List[str] = ["pkill atd"]
validation_mode = ConfigServiceMode.BLOCKING validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs = [] default_configs: List[Configuration] = []
modes = {} modes: Dict[str, Dict[str, str]] = {}
class HttpService(ConfigService): class HttpService(ConfigService):
name = "HTTP" name: str = "HTTP"
group = GROUP_NAME group: str = GROUP_NAME
directories = [ directories: List[str] = [
"/etc/apache2", "/etc/apache2",
"/var/run/apache2", "/var/run/apache2",
"/var/log/apache2", "/var/log/apache2",
@ -283,20 +268,22 @@ class HttpService(ConfigService):
"/var/lock/apache2", "/var/lock/apache2",
"/var/www", "/var/www",
] ]
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"] files: List[str] = [
executables = ["apache2ctl"] "/etc/apache2/apache2.conf",
dependencies = [] "/etc/apache2/envvars",
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"] "/var/www/index.html",
validate = ["pidof apache2"] ]
shutdown = ["apache2ctl stop"] executables: List[str] = ["apache2ctl"]
validation_mode = ConfigServiceMode.BLOCKING dependencies: List[str] = []
default_configs = [] startup: List[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
modes = {} validate: List[str] = ["pidof apache2"]
shutdown: List[str] = ["apache2ctl stop"]
validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
default_configs: List[Configuration] = []
modes: Dict[str, Dict[str, str]] = {}
def data(self) -> Dict[str, Any]: def data(self) -> Dict[str, Any]:
interfaces = [] ifaces = []
for ifc in self.node.netifs(): for iface in self.node.get_ifaces(control=False):
if getattr(ifc, "control", False): ifaces.append(iface)
continue return dict(ifaces=ifaces)
interfaces.append(ifc)
return dict(interfaces=interfaces)

View file

@ -5,8 +5,8 @@
<p>This is the default web page for this server.</p> <p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p> <p>The web server software is running but no content has been added, yet.</p>
<ul> <ul>
% for ifc in interfaces: % for iface in ifaces:
<li>${ifc.name} - ${ifc.addrlist}</li> <li>${iface.name} - ${iface.addrlist}</li>
% endfor % endfor
</ul> </ul>
</body> </body>

View file

@ -1,19 +1,3 @@
from core.utils import which
COREDPY_VERSION = "@PACKAGE_VERSION@" COREDPY_VERSION = "@PACKAGE_VERSION@"
CORE_CONF_DIR = "@CORE_CONF_DIR@" CORE_CONF_DIR = "@CORE_CONF_DIR@"
CORE_DATA_DIR = "@CORE_DATA_DIR@" CORE_DATA_DIR = "@CORE_DATA_DIR@"
QUAGGA_STATE_DIR = "@CORE_STATE_DIR@/run/quagga"
FRR_STATE_DIR = "@CORE_STATE_DIR@/run/frr"
VNODED_BIN = which("vnoded", required=True)
VCMD_BIN = which("vcmd", required=True)
SYSCTL_BIN = which("sysctl", required=True)
IP_BIN = which("ip", required=True)
ETHTOOL_BIN = which("ethtool", required=True)
TC_BIN = which("tc", required=True)
EBTABLES_BIN = which("ebtables", required=True)
MOUNT_BIN = which("mount", required=True)
UMOUNT_BIN = which("umount", required=True)
OVS_BIN = which("ovs-vsctl", required=False)
OVS_FLOW_BIN = which("ovs-ofctl", required=False)

View file

@ -1,6 +1,7 @@
""" """
EMANE Bypass model for CORE EMANE Bypass model for CORE
""" """
from typing import List, Set
from core.config import Configuration from core.config import Configuration
from core.emane import emanemodel from core.emane import emanemodel
@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel): class EmaneBypassModel(emanemodel.EmaneModel):
name = "emane_bypass" name: str = "emane_bypass"
# values to ignore, when writing xml files # values to ignore, when writing xml files
config_ignore = {"none"} config_ignore: Set[str] = {"none"}
# mac definitions # mac definitions
mac_library = "bypassmaclayer" mac_library: str = "bypassmaclayer"
mac_config = [ mac_config: List[Configuration] = [
Configuration( Configuration(
_id="none", _id="none",
_type=ConfigDataTypes.BOOL, _type=ConfigDataTypes.BOOL,
@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
] ]
# phy definitions # phy definitions
phy_library = "bypassphylayer" phy_library: str = "bypassphylayer"
phy_config = [] phy_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -10,8 +10,7 @@ from lxml import etree
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest, emanemodel from core.emane import emanemanifest, emanemodel
from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions
from core.emulator.enumerations import TransportType
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
@ -21,6 +20,7 @@ except ImportError:
try: try:
from emanesh.events.commeffectevent import CommEffectEvent from emanesh.events.commeffectevent import CommEffectEvent
except ImportError: except ImportError:
CommEffectEvent = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
@ -37,16 +37,15 @@ def convert_none(x: float) -> int:
class EmaneCommEffectModel(emanemodel.EmaneModel): class EmaneCommEffectModel(emanemodel.EmaneModel):
name = "emane_commeffect" name: str = "emane_commeffect"
shim_library: str = "commeffectshim"
shim_library = "commeffectshim" shim_xml: str = "commeffectshim.xml"
shim_xml = "commeffectshim.xml" shim_defaults: Dict[str, str] = {}
shim_defaults = {} config_shim: List[Configuration] = []
config_shim = []
# comm effect does not need the default phy and external configurations # comm effect does not need the default phy and external configurations
phy_config = [] phy_config: List[Configuration] = []
external_config = [] external_config: List[Configuration] = []
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:
@ -61,9 +60,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
def config_groups(cls) -> List[ConfigGroup]: def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
def build_xml_files( def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
""" """
Build the necessary nem and commeffect XMLs in the given path. Build the necessary nem and commeffect XMLs in the given path.
If an individual NEM has a nonstandard config, we need to build If an individual NEM has a nonstandard config, we need to build
@ -71,26 +68,19 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used. nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
:param config: emane model configuration for the node and interface :param config: emane model configuration for the node and interface
:param interface: interface for the emane node :param iface: interface for the emane node
:return: nothing :return: nothing
""" """
# retrieve xml names
nem_name = emanexml.nem_file_name(self, interface)
shim_name = emanexml.shim_file_name(self, interface)
# create and write nem document # create and write nem document
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured") nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
transport_type = TransportType.VIRTUAL transport_name = emanexml.transport_file_name(iface)
if interface and interface.transport_type == TransportType.RAW: etree.SubElement(nem_element, "transport", definition=transport_name)
transport_type = TransportType.RAW
transport_file = emanexml.transport_file_name(self.id, transport_type)
etree.SubElement(nem_element, "transport", definition=transport_file)
# set shim configuration # set shim configuration
nem_name = emanexml.nem_file_name(iface)
shim_name = emanexml.shim_file_name(iface)
etree.SubElement(nem_element, "shim", definition=shim_name) etree.SubElement(nem_element, "shim", definition=shim_name)
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_file(nem_element, "nem", nem_file)
# create and write shim document # create and write shim document
shim_element = etree.Element( shim_element = etree.Element(
@ -109,19 +99,13 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
ff = config["filterfile"] ff = config["filterfile"]
if ff.strip() != "": if ff.strip() != "":
emanexml.add_param(shim_element, "filterfile", ff) emanexml.add_param(shim_element, "filterfile", ff)
emanexml.create_iface_file(iface, shim_element, "shim", shim_name)
shim_file = os.path.join(self.session.session_dir, shim_name) # create transport xml
emanexml.create_file(shim_element, "shim", shim_file) emanexml.create_transport_xml(iface, config)
def linkconfig( def linkconfig(
self, self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
) -> None: ) -> None:
""" """
Generate CommEffect events when a Link Message is received having Generate CommEffect events when a Link Message is received having
@ -132,25 +116,23 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
logging.warning("%s: EMANE event service unavailable", self.name) logging.warning("%s: EMANE event service unavailable", self.name)
return return
if netif is None or netif2 is None: if iface is None or iface2 is None:
logging.warning("%s: missing NEM information", self.name) logging.warning("%s: missing NEM information", self.name)
return return
# TODO: batch these into multiple events per transmission # TODO: batch these into multiple events per transmission
# TODO: may want to split out seconds portion of delay and jitter # TODO: may want to split out seconds portion of delay and jitter
event = CommEffectEvent() event = CommEffectEvent()
emane_node = self.session.get_node(self.id, EmaneNet) nem1 = self.session.emane.get_nem_id(iface)
nemid = emane_node.getnemid(netif) nem2 = self.session.emane.get_nem_id(iface2)
nemid2 = emane_node.getnemid(netif2)
mbw = bw
logging.info("sending comm effect event") logging.info("sending comm effect event")
event.append( event.append(
nemid, nem1,
latency=convert_none(delay), latency=convert_none(options.delay),
jitter=convert_none(jitter), jitter=convert_none(options.jitter),
loss=convert_none(loss), loss=convert_none(options.loss),
duplicate=convert_none(duplicate), duplicate=convert_none(options.dup),
unicast=int(convert_none(bw)), unicast=int(convert_none(options.bandwidth)),
broadcast=int(convert_none(mbw)), broadcast=int(convert_none(options.bandwidth)),
) )
service.publish(nemid2, event) service.publish(nem2, event)

View file

@ -6,6 +6,7 @@ import logging
import os import os
import threading import threading
from collections import OrderedDict from collections import OrderedDict
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from core import utils from core import utils
@ -28,16 +29,14 @@ from core.emulator.enumerations import (
) )
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface, TunTap
from core.nodes.network import CtrlNet
from core.nodes.physical import Rj45Node
from core.xml import emanexml from core.xml import emanexml
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
try: try:
from emane.events import EventService from emane.events import EventService, PathlossEvent
from emane.events import LocationEvent from emane.events import LocationEvent
from emane.events.eventserviceexception import EventServiceException from emane.events.eventserviceexception import EventServiceException
except ImportError: except ImportError:
@ -48,6 +47,7 @@ except ImportError:
except ImportError: except ImportError:
EventService = None EventService = None
LocationEvent = None LocationEvent = None
PathlossEvent = None
EventServiceException = None EventServiceException = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
@ -62,6 +62,12 @@ DEFAULT_EMANE_PREFIX = "/usr"
DEFAULT_DEV = "ctrl0" DEFAULT_DEV = "ctrl0"
class EmaneState(Enum):
SUCCESS = 0
NOT_NEEDED = 1
NOT_READY = 2
class EmaneManager(ModelManager): class EmaneManager(ModelManager):
""" """
EMANE controller object. Lives in a Session instance and is used for EMANE controller object. Lives in a Session instance and is used for
@ -69,11 +75,11 @@ class EmaneManager(ModelManager):
controlling the EMANE daemons. controlling the EMANE daemons.
""" """
name = "emane" name: str = "emane"
config_type = RegisterTlvs.EMULATION_SERVER config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER
SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2) NOT_READY: int = 2
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG" EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL = 3 DEFAULT_LOG_LEVEL: int = 3
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
""" """
@ -83,73 +89,70 @@ class EmaneManager(ModelManager):
:return: nothing :return: nothing
""" """
super().__init__() super().__init__()
self.session = session self.session: "Session" = session
self._emane_nets = {} self.nems_to_ifaces: Dict[int, CoreInterface] = {}
self._emane_node_lock = threading.Lock() self.ifaces_to_nems: Dict[CoreInterface, int] = {}
self._emane_nets: Dict[int, EmaneNet] = {}
self._emane_node_lock: threading.Lock = threading.Lock()
# port numbers are allocated from these counters # port numbers are allocated from these counters
self.platformport = self.session.options.get_config_int( self.platformport: int = self.session.options.get_config_int(
"emane_platform_port", 8100 "emane_platform_port", 8100
) )
self.transformport = self.session.options.get_config_int( self.transformport: int = self.session.options.get_config_int(
"emane_transform_port", 8200 "emane_transform_port", 8200
) )
self.doeventloop = False self.doeventloop: bool = False
self.eventmonthread = None self.eventmonthread: Optional[threading.Thread] = None
# model for global EMANE configuration options # model for global EMANE configuration options
self.emane_config = EmaneGlobalModel(session) self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session)
self.set_configs(self.emane_config.default_values()) self.set_configs(self.emane_config.default_values())
# link monitor # link monitor
self.link_monitor = EmaneLinkMonitor(self) self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
self.service = None self.service: Optional[EventService] = None
self.eventchannel = None self.eventchannel: Optional[Tuple[str, int, str]] = None
self.event_device = None self.event_device: Optional[str] = None
self.emane_check() self.emane_check()
def getifcconfig( def next_nem_id(self) -> int:
self, node_id: int, interface: CoreInterface, model_name: str nem_id = int(self.get_config("nem_id_start"))
while nem_id in self.nems_to_ifaces:
nem_id += 1
return nem_id
def get_iface_config(
self, emane_net: EmaneNet, iface: CoreInterface
) -> Dict[str, str]: ) -> Dict[str, str]:
""" """
Retrieve interface configuration or node configuration if not provided. Retrieve configuration for a given interface.
:param node_id: node id :param emane_net: emane network the interface is connected to
:param interface: node interface :param iface: interface running emane
:param model_name: model to get configuration for :return: net, node, or interface model configuration
:return: node/interface model configuration
""" """
# use the network-wide config values or interface(NEM)-specific values? model_name = emane_net.model.name
if interface is None:
return self.get_configs(node_id=node_id, config_type=model_name)
else:
# don"t use default values when interface config is the same as net # don"t use default values when interface config is the same as net
# note here that using ifc.node.id as key allows for only one type # note here that using iface.node.id as key allows for only one type
# of each model per node; # of each model per node;
# TODO: use both node and interface as key # TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:iface.name"
# Adamson change: first check for iface config keyed by "node:ifc.name"
# (so that nodes w/ multiple interfaces of same conftype can have # (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface) # different configs for each separate interface)
key = 1000 * interface.node.id key = 1000 * iface.node.id
if interface.netindex is not None: if iface.node_id is not None:
key += interface.netindex key += iface.node_id
# try retrieve interface specific configuration, avoid getting defaults # try retrieve interface specific configuration, avoid getting defaults
config = self.get_configs(node_id=key, config_type=model_name) config = self.get_configs(node_id=key, config_type=model_name)
# otherwise retrieve the interfaces node configuration, avoid using defaults # otherwise retrieve the interfaces node configuration, avoid using defaults
if not config: if not config:
config = self.get_configs( config = self.get_configs(node_id=iface.node.id, config_type=model_name)
node_id=interface.node.id, config_type=model_name
)
# get non interface config, when none found # get non interface config, when none found
if not config: if not config:
# with EMANE 0.9.2+, we need an extra NEM XML from # with EMANE 0.9.2+, we need an extra NEM XML from
# model.buildnemxmlfiles(), so defaults are returned here # model.buildnemxmlfiles(), so defaults are returned here
config = self.get_configs(node_id=node_id, config_type=model_name) config = self.get_configs(node_id=emane_net.id, config_type=model_name)
return config return config
def config_reset(self, node_id: int = None) -> None: def config_reset(self, node_id: int = None) -> None:
@ -162,12 +165,15 @@ class EmaneManager(ModelManager):
:return: nothing :return: nothing
""" """
try:
# check for emane # check for emane
args = "emane --version" path = utils.which("emane", required=False)
emane_version = utils.cmd(args) if not path:
logging.info("using EMANE: %s", emane_version) logging.info("emane is not installed")
self.session.distributed.execute(lambda x: x.remote_cmd(args)) return
# get version
emane_version = utils.cmd("emane --version")
logging.info("using emane: %s", emane_version)
# load default emane models # load default emane models
self.load_models(EMANE_MODELS) self.load_models(EMANE_MODELS)
@ -177,8 +183,6 @@ class EmaneManager(ModelManager):
if custom_models_path: if custom_models_path:
emane_models = utils.load_classes(custom_models_path, EmaneModel) emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models) self.load_models(emane_models)
except CoreCommandError:
logging.info("emane is not installed")
def deleteeventservice(self) -> None: def deleteeventservice(self) -> None:
if self.service: if self.service:
@ -249,8 +253,8 @@ class EmaneManager(ModelManager):
""" """
with self._emane_node_lock: with self._emane_node_lock:
if emane_net.id in self._emane_nets: if emane_net.id in self._emane_nets:
raise KeyError( raise CoreError(
f"non-unique EMANE object id {emane_net.id} for {emane_net}" f"duplicate emane network({emane_net.id}): {emane_net.name}"
) )
self._emane_nets[emane_net.id] = emane_net self._emane_nets[emane_net.id] = emane_net
@ -259,14 +263,13 @@ class EmaneManager(ModelManager):
Return a set of CoreNodes that are linked to an EMANE network, Return a set of CoreNodes that are linked to an EMANE network,
e.g. containers having one or more radio interfaces. e.g. containers having one or more radio interfaces.
""" """
# assumes self._objslock already held
nodes = set() nodes = set()
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
for netif in emane_net.netifs(): for iface in emane_net.get_ifaces():
nodes.add(netif.node) nodes.add(iface.node)
return nodes return nodes
def setup(self) -> int: def setup(self) -> EmaneState:
""" """
Setup duties for EMANE manager. Setup duties for EMANE manager.
@ -274,9 +277,7 @@ class EmaneManager(ModelManager):
instantiation instantiation
""" """
logging.debug("emane setup") logging.debug("emane setup")
with self.session.nodes_lock:
# TODO: drive this from the session object
with self.session._nodes_lock:
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
if isinstance(node, EmaneNet): if isinstance(node, EmaneNet):
@ -284,10 +285,9 @@ class EmaneManager(ModelManager):
"adding emane node: id(%s) name(%s)", node.id, node.name "adding emane node: id(%s) name(%s)", node.id, node.name
) )
self.add_node(node) self.add_node(node)
if not self._emane_nets: if not self._emane_nets:
logging.debug("no emane nodes in session") logging.debug("no emane nodes in session")
return EmaneManager.NOT_NEEDED return EmaneState.NOT_NEEDED
# check if bindings were installed # check if bindings were installed
if EventService is None: if EventService is None:
@ -303,7 +303,7 @@ class EmaneManager(ModelManager):
"EMANE cannot start, check core config. invalid OTA device provided: %s", "EMANE cannot start, check core config. invalid OTA device provided: %s",
otadev, otadev,
) )
return EmaneManager.NOT_READY return EmaneState.NOT_READY
self.session.add_remove_control_net( self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False net_index=netidx, remove=False, conf_required=False
@ -315,19 +315,18 @@ class EmaneManager(ModelManager):
logging.debug("emane event service device index: %s", netidx) logging.debug("emane event service device index: %s", netidx)
if netidx < 0: if netidx < 0:
logging.error( logging.error(
"EMANE cannot start, check core config. invalid event service device: %s", "emane cannot start due to invalid event service device: %s",
eventdev, eventdev,
) )
return EmaneManager.NOT_READY return EmaneState.NOT_READY
self.session.add_remove_control_net( self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False net_index=netidx, remove=False, conf_required=False
) )
self.check_node_models() self.check_node_models()
return EmaneManager.SUCCESS return EmaneState.SUCCESS
def startup(self) -> int: def startup(self) -> EmaneState:
""" """
After all the EMANE networks have been added, build XML files After all the EMANE networks have been added, build XML files
and start the daemons. and start the daemons.
@ -336,39 +335,63 @@ class EmaneManager(ModelManager):
instantiation instantiation
""" """
self.reset() self.reset()
r = self.setup() status = self.setup()
if status != EmaneState.SUCCESS:
# NOT_NEEDED or NOT_READY return status
if r != EmaneManager.SUCCESS:
return r
nems = []
with self._emane_node_lock:
self.buildxml()
self.starteventmonitor() self.starteventmonitor()
self.buildeventservicexml()
if self.numnems() > 0: with self._emane_node_lock:
self.startdaemons() logging.info("emane building xmls...")
self.installnetifs() for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
for node_id in self._emane_nets: if not emane_net.model:
emane_node = self._emane_nets[node_id] logging.error("emane net(%s) has no model", emane_net.name)
for netif in emane_node.netifs(): continue
nems.append( for iface in emane_net.get_ifaces():
(netif.node.name, netif.name, emane_node.getnemid(netif)) self.start_iface(emane_net, iface)
)
if nems:
emane_nems_filename = os.path.join(self.session.session_dir, "emane_nems")
try:
with open(emane_nems_filename, "w") as f:
for nodename, ifname, nemid in nems:
f.write(f"{nodename} {ifname} {nemid}\n")
except IOError:
logging.exception("Error writing EMANE NEMs file: %s")
if self.links_enabled(): if self.links_enabled():
self.link_monitor.start() self.link_monitor.start()
return EmaneManager.SUCCESS return EmaneState.SUCCESS
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
if not iface.node:
logging.error(
"emane net(%s) connected interface(%s) missing node",
emane_net.name,
iface.name,
)
return
control_net = self.session.add_remove_control_net(
0, remove=False, conf_required=False
)
nem_id = self.next_nem_id()
self.set_nem(nem_id, iface)
self.write_nem(iface, nem_id)
emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id)
config = self.get_iface_config(emane_net, iface)
emane_net.model.build_xml_files(config, iface)
self.start_daemon(iface)
self.install_iface(emane_net, iface)
def set_nem(self, nem_id: int, iface: CoreInterface) -> None:
if nem_id in self.nems_to_ifaces:
raise CoreError(f"adding duplicate nem: {nem_id}")
self.nems_to_ifaces[nem_id] = iface
self.ifaces_to_nems[iface] = nem_id
def get_iface(self, nem_id: int) -> Optional[CoreInterface]:
return self.nems_to_ifaces.get(nem_id)
def get_nem_id(self, iface: CoreInterface) -> Optional[int]:
return self.ifaces_to_nems.get(iface)
def write_nem(self, iface: CoreInterface, nem_id: int) -> None:
path = os.path.join(self.session.session_dir, "emane_nems")
try:
with open(path, "a") as f:
f.write(f"{iface.node.name} {iface.name} {nem_id}\n")
except IOError:
logging.exception("error writing to emane nem file")
def links_enabled(self) -> bool: def links_enabled(self) -> bool:
return self.get_config("link_enabled") == "1" return self.get_config("link_enabled") == "1"
@ -379,18 +402,15 @@ class EmaneManager(ModelManager):
""" """
if not self.genlocationevents(): if not self.genlocationevents():
return return
with self._emane_node_lock: with self._emane_node_lock:
for key in sorted(self._emane_nets.keys()): for node_id in sorted(self._emane_nets):
emane_node = self._emane_nets[key] emane_net = self._emane_nets[node_id]
logging.debug( logging.debug(
"post startup for emane node: %s - %s", "post startup for emane node: %s - %s", emane_net.id, emane_net.name
emane_node.id,
emane_node.name,
) )
emane_node.model.post_startup() emane_net.model.post_startup()
for netif in emane_node.netifs(): for iface in emane_net.get_ifaces():
netif.setposition() iface.setposition()
def reset(self) -> None: def reset(self) -> None:
""" """
@ -399,13 +419,8 @@ class EmaneManager(ModelManager):
""" """
with self._emane_node_lock: with self._emane_node_lock:
self._emane_nets.clear() self._emane_nets.clear()
self.nems_to_ifaces.clear()
self.platformport = self.session.options.get_config_int( self.ifaces_to_nems.clear()
"emane_platform_port", 8100
)
self.transformport = self.session.options.get_config_int(
"emane_transform_port", 8200
)
def shutdown(self) -> None: def shutdown(self) -> None:
""" """
@ -417,44 +432,27 @@ class EmaneManager(ModelManager):
logging.info("stopping EMANE daemons") logging.info("stopping EMANE daemons")
if self.links_enabled(): if self.links_enabled():
self.link_monitor.stop() self.link_monitor.stop()
self.deinstallnetifs() self.deinstall_ifaces()
self.stopdaemons() self.stopdaemons()
self.stopeventmonitor() self.stopeventmonitor()
def buildxml(self) -> None:
"""
Build XML files required to run EMANE on each node.
NEMs run inside containers using the control network for passing
events and data.
"""
# assume self._objslock is already held here
logging.info("emane building xml...")
# on master, control network bridge added earlier in startup()
ctrlnet = self.session.add_remove_control_net(
net_index=0, remove=False, conf_required=False
)
self.buildplatformxml(ctrlnet)
self.buildnemxml()
self.buildeventservicexml()
def check_node_models(self) -> None: def check_node_models(self) -> None:
""" """
Associate EMANE model classes with EMANE network nodes. Associate EMANE model classes with EMANE network nodes.
""" """
for node_id in self._emane_nets: for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id] emane_net = self._emane_nets[node_id]
logging.debug("checking emane model for node: %s", node_id) logging.debug("checking emane model for node: %s", node_id)
# skip nodes that already have a model set # skip nodes that already have a model set
if emane_node.model: if emane_net.model:
logging.debug( logging.debug(
"node(%s) already has model(%s)", "node(%s) already has model(%s)", emane_net.id, emane_net.model.name
emane_node.id,
emane_node.model.name,
) )
continue continue
# set model configured for node, due to legacy messaging configuration before nodes exist # set model configured for node, due to legacy messaging configuration
# before nodes exist
model_name = self.node_models.get(node_id) model_name = self.node_models.get(node_id)
if not model_name: if not model_name:
logging.error("emane node(%s) has no node model", node_id) logging.error("emane node(%s) has no node model", node_id)
@ -463,81 +461,34 @@ class EmaneManager(ModelManager):
config = self.get_model_config(node_id=node_id, model_name=model_name) config = self.get_model_config(node_id=node_id, model_name=model_name)
logging.debug("setting emane model(%s) config(%s)", model_name, config) logging.debug("setting emane model(%s) config(%s)", model_name, config)
model_class = self.models[model_name] model_class = self.models[model_name]
emane_node.setmodel(model_class, config) emane_net.setmodel(model_class, config)
def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]:
"""
Look for the given numerical NEM ID and return the first matching
EMANE network and NEM interface.
"""
emane_node = None
netif = None
for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id]
netif = emane_node.getnemnetif(nemid)
if netif is not None:
break
else:
emane_node = None
return emane_node, netif
def get_nem_link( def get_nem_link(
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
) -> Optional[LinkData]: ) -> Optional[LinkData]:
emane1, netif = self.nemlookup(nem1) iface1 = self.get_iface(nem1)
if not emane1 or not netif: if not iface1:
logging.error("invalid nem: %s", nem1) logging.error("invalid nem: %s", nem1)
return None return None
node1 = netif.node node1 = iface1.node
emane2, netif = self.nemlookup(nem2) iface2 = self.get_iface(nem2)
if not emane2 or not netif: if not iface2:
logging.error("invalid nem: %s", nem2) logging.error("invalid nem: %s", nem2)
return None return None
node2 = netif.node node2 = iface2.node
color = self.session.get_link_color(emane1.id) if iface1.net != iface2.net:
return None
emane_net = iface1.net
color = self.session.get_link_color(emane_net.id)
return LinkData( return LinkData(
message_type=flags, message_type=flags,
type=LinkTypes.WIRELESS,
node1_id=node1.id, node1_id=node1.id,
node2_id=node2.id, node2_id=node2.id,
network_id=emane1.id, network_id=emane_net.id,
link_type=LinkTypes.WIRELESS,
color=color, color=color,
) )
def numnems(self) -> int:
"""
Return the number of NEMs emulated locally.
"""
count = 0
for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id]
count += len(emane_node.netifs())
return count
def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
"""
Build a platform.xml file now that all nodes are configured.
"""
nemid = int(self.get_config("nem_id_start"))
platform_xmls = {}
# assume self._objslock is already held here
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
nemid = emanexml.build_node_platform_xml(
self, ctrlnet, emane_node, nemid, platform_xmls
)
def buildnemxml(self) -> None:
"""
Builds the nem, mac, and phy xml files for each EMANE network.
"""
for key in sorted(self._emane_nets):
emane_net = self._emane_nets[key]
emanexml.build_xml_files(self, emane_net)
def buildeventservicexml(self) -> None: def buildeventservicexml(self) -> None:
""" """
Build the libemaneeventservice.xml file if event service options Build the libemaneeventservice.xml file if event service options
@ -570,7 +521,7 @@ class EmaneManager(ModelManager):
) )
) )
def startdaemons(self) -> None: def start_daemon(self, iface: CoreInterface) -> None:
""" """
Start one EMANE daemon per node having a radio. Start one EMANE daemon per node having a radio.
Add a control network even if the user has not configured one. Add a control network even if the user has not configured one.
@ -580,116 +531,91 @@ class EmaneManager(ModelManager):
cfgloglevel = self.session.options.get_config_int("emane_log_level") cfgloglevel = self.session.options.get_config_int("emane_log_level")
realtime = self.session.options.get_config_bool("emane_realtime", default=True) realtime = self.session.options.get_config_bool("emane_realtime", default=True)
if cfgloglevel: if cfgloglevel:
logging.info("setting user-defined EMANE log level: %d", cfgloglevel) logging.info("setting user-defined emane log level: %d", cfgloglevel)
loglevel = str(cfgloglevel) loglevel = str(cfgloglevel)
emanecmd = f"emane -d -l {loglevel}" emanecmd = f"emane -d -l {loglevel}"
if realtime: if realtime:
emanecmd += " -r" emanecmd += " -r"
node = iface.node
if iface.is_virtual():
otagroup, _otaport = self.get_config("otamanagergroup").split(":") otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice") otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev) otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":") eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice") eventdev = self.get_config("eventservicedevice")
eventservicenetidx = self.session.get_control_net_index(eventdev) eventservicenetidx = self.session.get_control_net_index(eventdev)
run_emane_on_host = False
for node in self.getnodes():
if isinstance(node, Rj45Node):
run_emane_on_host = True
continue
path = self.session.session_dir
n = node.id
# control network not yet started here # control network not yet started here
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, 0, remove=False, conf_required=False node, 0, remove=False, conf_required=False
) )
if otanetidx > 0: if otanetidx > 0:
logging.info("adding ota device ctrl%d", otanetidx) logging.info("adding ota device ctrl%d", otanetidx)
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, otanetidx, remove=False, conf_required=False node, otanetidx, remove=False, conf_required=False
) )
if eventservicenetidx >= 0: if eventservicenetidx >= 0:
logging.info("adding event service device ctrl%d", eventservicenetidx) logging.info("adding event service device ctrl%d", eventservicenetidx)
self.session.add_remove_control_interface( self.session.add_remove_control_iface(
node, eventservicenetidx, remove=False, conf_required=False node, eventservicenetidx, remove=False, conf_required=False
) )
# multicast route is needed for OTA data # multicast route is needed for OTA data
logging.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev)
node.node_net_client.create_route(otagroup, otadev) node.node_net_client.create_route(otagroup, otadev)
# multicast route is also needed for event data if on control network # multicast route is also needed for event data if on control network
if eventservicenetidx >= 0 and eventgroup != otagroup: if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev) node.node_net_client.create_route(eventgroup, eventdev)
# start emane # start emane
log_file = os.path.join(path, f"emane{n}.log") log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, f"platform{n}.xml") platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml")
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
output = node.cmd(args) node.cmd(args)
logging.info("node(%s) emane daemon running: %s", node.name, args) logging.info("node(%s) emane daemon running: %s", node.name, args)
logging.debug("node(%s) emane daemon output: %s", node.name, output) else:
if not run_emane_on_host:
return
path = self.session.session_dir path = self.session.session_dir
log_file = os.path.join(path, "emane.log") log_file = os.path.join(path, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, "platform.xml") platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
emanecmd += f" -f {log_file} {platform_xml}" emanecmd += f" -f {log_file} {platform_xml}"
utils.cmd(emanecmd, cwd=path) node.host_cmd(emanecmd, cwd=path)
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path)) logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
logging.info("host emane daemon running: %s", emanecmd)
def stopdaemons(self) -> None: def stopdaemons(self) -> None:
""" """
Kill the appropriate EMANE daemons. Kill the appropriate EMANE daemons.
""" """
# TODO: we may want to improve this if we had the PIDs from the specific EMANE
# daemons that we"ve started
kill_emaned = "killall -q emane" kill_emaned = "killall -q emane"
kill_transortd = "killall -q emanetransportd" for node_id in sorted(self._emane_nets):
stop_emane_on_host = False emane_net = self._emane_nets[node_id]
for node in self.getnodes(): for iface in emane_net.get_ifaces():
if isinstance(node, Rj45Node): node = iface.node
stop_emane_on_host = True if not node.up:
continue continue
if iface.is_raw():
if node.up: node.host_cmd(kill_emaned, wait=False)
else:
node.cmd(kill_emaned, wait=False) node.cmd(kill_emaned, wait=False)
# TODO: RJ45 node
if stop_emane_on_host: def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
try: config = self.get_iface_config(emane_net, iface)
utils.cmd(kill_emaned) external = config.get("external", "0")
utils.cmd(kill_transortd) if isinstance(iface, TunTap) and external == "0":
self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned)) iface.set_ips()
self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd)) # at this point we register location handlers for generating
except CoreCommandError: # EMANE location events
logging.exception("error shutting down emane daemons") if self.genlocationevents():
iface.poshook = emane_net.setnemposition
iface.setposition()
def installnetifs(self) -> None: def deinstall_ifaces(self) -> None:
"""
Install TUN/TAP virtual interfaces into their proper namespaces
now that the EMANE daemons are running.
"""
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
logging.info("emane install netifs for node: %d", key)
emane_node.installnetifs()
def deinstallnetifs(self) -> None:
""" """
Uninstall TUN/TAP virtual interfaces. Uninstall TUN/TAP virtual interfaces.
""" """
for key in sorted(self._emane_nets.keys()): for key in sorted(self._emane_nets):
emane_node = self._emane_nets[key] emane_net = self._emane_nets[key]
emane_node.deinstallnetifs() for iface in emane_net.get_ifaces():
if iface.is_virtual():
iface.shutdown()
iface.poshook = None
def doeventmonitor(self) -> bool: def doeventmonitor(self) -> bool:
""" """
@ -717,7 +643,6 @@ class EmaneManager(ModelManager):
logging.info("emane start event monitor") logging.info("emane start event monitor")
if not self.doeventmonitor(): if not self.doeventmonitor():
return return
if self.service is None: if self.service is None:
logging.error( logging.error(
"Warning: EMANE events will not be generated " "Warning: EMANE events will not be generated "
@ -805,12 +730,12 @@ class EmaneManager(ModelManager):
Returns True if successfully parsed and a Node Message was sent. Returns True if successfully parsed and a Node Message was sent.
""" """
# convert nemid to node number # convert nemid to node number
_emanenode, netif = self.nemlookup(nemid) iface = self.get_iface(nemid)
if netif is None: if iface is None:
logging.info("location event for unknown NEM %s", nemid) logging.info("location event for unknown NEM %s", nemid)
return False return False
n = netif.node.id n = iface.node.id
# convert from lat/long/alt to x,y,z coordinates # convert from lat/long/alt to x,y,z coordinates
x, y, z = self.session.location.getxyz(lat, lon, alt) x, y, z = self.session.location.getxyz(lat, lon, alt)
x = int(x) x = int(x)
@ -868,18 +793,33 @@ class EmaneManager(ModelManager):
result = False result = False
return result return result
def publish_pathloss(self, nem1: int, nem2: int, rx1: float, rx2: float) -> None:
"""
Publish pathloss events between provided nems, using provided rx power.
:param nem1: interface one for pathloss
:param nem2: interface two for pathloss
:param rx1: received power from nem2 to nem1
:param rx2: received power from nem1 to nem2
:return: nothing
"""
event = PathlossEvent()
event.append(nem1, forward=rx1)
event.append(nem2, forward=rx2)
self.service.publish(nem1, event)
self.service.publish(nem2, event)
class EmaneGlobalModel: class EmaneGlobalModel:
""" """
Global EMANE configuration options. Global EMANE configuration options.
""" """
name = "emane" name: str = "emane"
bitmap = None bitmap: Optional[str] = None
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
self.session = session self.session: "Session" = session
self.core_config = [ self.core_config: List[Configuration] = [
Configuration( Configuration(
_id="platform_id_start", _id="platform_id_start",
_type=ConfigDataTypes.INT32, _type=ConfigDataTypes.INT32,

View file

@ -11,6 +11,7 @@ except ImportError:
try: try:
from emanesh import manifest from emanesh import manifest
except ImportError: except ImportError:
manifest = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")

View file

@ -3,14 +3,16 @@ Defines Emane Models used within CORE.
""" """
import logging import logging
import os import os
from typing import Dict, List from typing import Dict, List, Optional, Set
from core.config import ConfigGroup, Configuration from core.config import ConfigGroup, Configuration
from core.emane import emanemanifest from core.emane import emanemanifest
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.enumerations import ConfigDataTypes, TransportType from core.emulator.data import LinkOptions
from core.emulator.enumerations import ConfigDataTypes
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel
from core.nodes.base import CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
@ -23,19 +25,23 @@ class EmaneModel(WirelessModel):
""" """
# default mac configuration settings # default mac configuration settings
mac_library = None mac_library: Optional[str] = None
mac_xml = None mac_xml: Optional[str] = None
mac_defaults = {} mac_defaults: Dict[str, str] = {}
mac_config = [] mac_config: List[Configuration] = []
# default phy configuration settings, using the universal model # default phy configuration settings, using the universal model
phy_library = None phy_library: Optional[str] = None
phy_xml = "emanephy.xml" phy_xml: str = "emanephy.xml"
phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"} phy_defaults: Dict[str, str] = {
phy_config = [] "subid": "1",
"propagationmodel": "2ray",
"noisemode": "none",
}
phy_config: List[Configuration] = []
# support for external configurations # support for external configurations
external_config = [ external_config: List[Configuration] = [
Configuration("external", ConfigDataTypes.BOOL, default="0"), Configuration("external", ConfigDataTypes.BOOL, default="0"),
Configuration( Configuration(
"platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001" "platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001"
@ -45,7 +51,7 @@ class EmaneModel(WirelessModel):
), ),
] ]
config_ignore = set() config_ignore: Set[str] = set()
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:
@ -90,45 +96,20 @@ class EmaneModel(WirelessModel):
ConfigGroup("External Parameters", phy_len + 1, config_len), ConfigGroup("External Parameters", phy_len + 1, config_len),
] ]
def build_xml_files( def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
""" """
Builds xml files for this emane model. Creates a nem.xml file that points to Builds xml files for this emane model. Creates a nem.xml file that points to
both mac.xml and phy.xml definitions. both mac.xml and phy.xml definitions.
:param config: emane model configuration for the node and interface :param config: emane model configuration for the node and interface
:param interface: interface for the emane node :param iface: interface to run emane for
:return: nothing :return: nothing
""" """
nem_name = emanexml.nem_file_name(self, interface) # create nem, mac, and phy xml files
mac_name = emanexml.mac_file_name(self, interface) emanexml.create_nem_xml(self, iface, config)
phy_name = emanexml.phy_file_name(self, interface) emanexml.create_mac_xml(self, iface, config)
emanexml.create_phy_xml(self, iface, config)
# remote server for file emanexml.create_transport_xml(iface, config)
server = None
if interface is not None:
server = interface.node.server
# check if this is external
transport_type = TransportType.VIRTUAL
if interface and interface.transport_type == TransportType.RAW:
transport_type = TransportType.RAW
transport_name = emanexml.transport_file_name(self.id, transport_type)
# create nem xml file
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_nem_xml(
self, config, nem_file, transport_name, mac_name, phy_name, server
)
# create mac xml file
mac_file = os.path.join(self.session.session_dir, mac_name)
emanexml.create_mac_xml(self, config, mac_file, server)
# create phy xml file
phy_file = os.path.join(self.session.session_dir, phy_name)
emanexml.create_phy_xml(self, config, phy_file, server)
def post_startup(self) -> None: def post_startup(self) -> None:
""" """
@ -138,42 +119,31 @@ class EmaneModel(WirelessModel):
""" """
logging.debug("emane model(%s) has no post setup tasks", self.name) logging.debug("emane model(%s) has no post setup tasks", self.name)
def update(self, moved: bool, moved_netifs: List[CoreInterface]) -> None: def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
""" """
Invoked from MobilityModel when nodes are moved; this causes Invoked from MobilityModel when nodes are moved; this causes
emane location events to be generated for the nodes in the moved emane location events to be generated for the nodes in the moved
list, making EmaneModels compatible with Ns2ScriptedMobility. list, making EmaneModels compatible with Ns2ScriptedMobility.
:param moved: were nodes moved :param moved: moved nodes
:param moved_netifs: interfaces that were moved :param moved_ifaces: interfaces that were moved
:return: nothing :return: nothing
""" """
try: try:
wlan = self.session.get_node(self.id, EmaneNet) wlan = self.session.get_node(self.id, EmaneNet)
wlan.setnempositions(moved_netifs) wlan.setnempositions(moved_ifaces)
except CoreError: except CoreError:
logging.exception("error during update") logging.exception("error during update")
def linkconfig( def linkconfig(
self, self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
) -> None: ) -> None:
""" """
Invoked when a Link Message is received. Default is unimplemented. Invoked when a Link Message is received. Default is unimplemented.
:param netif: interface one :param iface: interface one
:param bw: bandwidth to set to :param options: options for configuring link
:param delay: packet delay to set to :param iface2: interface two
:param loss: packet loss to set to
:param duplicate: duplicate percentage to set to
:param jitter: jitter to set to
:param netif2: interface two
:return: nothing :return: nothing
""" """
logging.warning("emane model(%s) does not support link config", self.name) logging.warning("emane model(%s) does not support link config", self.name)

View file

@ -8,11 +8,11 @@ from core.emane import emanemodel
class EmaneIeee80211abgModel(emanemodel.EmaneModel): class EmaneIeee80211abgModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_ieee80211abg" name: str = "emane_ieee80211abg"
# mac configuration # mac configuration
mac_library = "ieee80211abgmaclayer" mac_library: str = "ieee80211abgmaclayer"
mac_xml = "ieee80211abgmaclayer.xml" mac_xml: str = "ieee80211abgmaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -2,9 +2,8 @@ import logging
import sched import sched
import threading import threading
import time import time
from typing import TYPE_CHECKING, Dict, List, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import netaddr
from lxml import etree from lxml import etree
from core.emulator.data import LinkData from core.emulator.data import LinkData
@ -17,28 +16,29 @@ except ImportError:
try: try:
from emanesh import shell from emanesh import shell
except ImportError: except ImportError:
shell = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager from core.emane.emanemanager import EmaneManager
DEFAULT_PORT = 47_000 DEFAULT_PORT: int = 47_000
MAC_COMPONENT_INDEX = 1 MAC_COMPONENT_INDEX: int = 1
EMANE_RFPIPE = "rfpipemaclayer" EMANE_RFPIPE: str = "rfpipemaclayer"
EMANE_80211 = "ieee80211abgmaclayer" EMANE_80211: str = "ieee80211abgmaclayer"
EMANE_TDMA = "tdmaeventschedulerradiomodel" EMANE_TDMA: str = "tdmaeventschedulerradiomodel"
SINR_TABLE = "NeighborStatusTable" SINR_TABLE: str = "NeighborStatusTable"
NEM_SELF = 65535 NEM_SELF: int = 65535
class LossTable: class LossTable:
def __init__(self, losses: Dict[float, float]) -> None: def __init__(self, losses: Dict[float, float]) -> None:
self.losses = losses self.losses: Dict[float, float] = losses
self.sinrs = sorted(self.losses.keys()) self.sinrs: List[float] = sorted(self.losses.keys())
self.loss_lookup = {} self.loss_lookup: Dict[int, float] = {}
for index, value in enumerate(self.sinrs): for index, value in enumerate(self.sinrs):
self.loss_lookup[index] = self.losses[value] self.loss_lookup[index] = self.losses[value]
self.mac_id = None self.mac_id: Optional[str] = None
def get_loss(self, sinr: float) -> float: def get_loss(self, sinr: float) -> float:
index = self._get_index(sinr) index = self._get_index(sinr)
@ -54,11 +54,11 @@ class LossTable:
class EmaneLink: class EmaneLink:
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None: def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
self.from_nem = from_nem self.from_nem: int = from_nem
self.to_nem = to_nem self.to_nem: int = to_nem
self.sinr = sinr self.sinr: float = sinr
self.last_seen = None self.last_seen: Optional[float] = None
self.updated = False self.updated: bool = False
self.touch() self.touch()
def update(self, sinr: float) -> None: def update(self, sinr: float) -> None:
@ -78,9 +78,11 @@ class EmaneLink:
class EmaneClient: class EmaneClient:
def __init__(self, address: str) -> None: def __init__(self, address: str) -> None:
self.address = address self.address: str = address
self.client = shell.ControlPortClient(self.address, DEFAULT_PORT) self.client: shell.ControlPortClient = shell.ControlPortClient(
self.nems = {} self.address, DEFAULT_PORT
)
self.nems: Dict[int, LossTable] = {}
self.setup() self.setup()
def setup(self) -> None: def setup(self) -> None:
@ -174,15 +176,15 @@ class EmaneClient:
class EmaneLinkMonitor: class EmaneLinkMonitor:
def __init__(self, emane_manager: "EmaneManager") -> None: def __init__(self, emane_manager: "EmaneManager") -> None:
self.emane_manager = emane_manager self.emane_manager: "EmaneManager" = emane_manager
self.clients = [] self.clients: List[EmaneClient] = []
self.links = {} self.links: Dict[Tuple[int, int], EmaneLink] = {}
self.complete_links = set() self.complete_links: Set[Tuple[int, int]] = set()
self.loss_threshold = None self.loss_threshold: Optional[int] = None
self.link_interval = None self.link_interval: Optional[int] = None
self.link_timeout = None self.link_timeout: Optional[int] = None
self.scheduler = None self.scheduler: Optional[sched.scheduler] = None
self.running = False self.running: bool = False
def start(self) -> None: def start(self) -> None:
self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) self.loss_threshold = int(self.emane_manager.get_config("loss_threshold"))
@ -209,15 +211,12 @@ class EmaneLinkMonitor:
addresses = [] addresses = []
nodes = self.emane_manager.getnodes() nodes = self.emane_manager.getnodes()
for node in nodes: for node in nodes:
for netif in node.netifs(): for iface in node.get_ifaces():
if isinstance(netif.net, CtrlNet): if isinstance(iface.net, CtrlNet):
ip4 = None ip4 = iface.get_ip4()
for x in netif.addrlist:
address, prefix = x.split("/")
if netaddr.valid_ipv4(address):
ip4 = address
if ip4: if ip4:
addresses.append(ip4) address = str(ip4.ip)
addresses.append(address)
break break
return addresses return addresses
@ -266,11 +265,11 @@ class EmaneLinkMonitor:
self.scheduler.enter(self.link_interval, 0, self.check_links) self.scheduler.enter(self.link_interval, 0, self.check_links)
def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]: def get_complete_id(self, link_id: Tuple[int, int]) -> Tuple[int, int]:
value_one, value_two = link_id value1, value2 = link_id
if value_one < value_two: if value1 < value2:
return value_one, value_two return value1, value2
else: else:
return value_two, value_one return value2, value1
def is_complete_link(self, link_id: Tuple[int, int]) -> bool: def is_complete_link(self, link_id: Tuple[int, int]) -> bool:
reverse_id = link_id[1], link_id[0] reverse_id = link_id[1], link_id[0]
@ -284,8 +283,8 @@ class EmaneLinkMonitor:
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}" return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None: def send_link(self, message_type: MessageFlags, link_id: Tuple[int, int]) -> None:
nem_one, nem_two = link_id nem1, nem2 = link_id
link = self.emane_manager.get_nem_link(nem_one, nem_two, message_type) link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
if link: if link:
label = self.get_link_label(link_id) label = self.get_link_label(link_id)
link.label = label link.label = label
@ -295,18 +294,18 @@ class EmaneLinkMonitor:
self, self,
message_type: MessageFlags, message_type: MessageFlags,
label: str, label: str,
node_one: int, node1: int,
node_two: int, node2: int,
emane_id: int, emane_id: int,
) -> None: ) -> None:
color = self.emane_manager.session.get_link_color(emane_id) color = self.emane_manager.session.get_link_color(emane_id)
link_data = LinkData( link_data = LinkData(
message_type=message_type, message_type=message_type,
type=LinkTypes.WIRELESS,
label=label, label=label,
node1_id=node_one, node1_id=node1,
node2_id=node_two, node2_id=node2,
network_id=emane_id, network_id=emane_id,
link_type=LinkTypes.WIRELESS,
color=color, color=color,
) )
self.emane_manager.session.broadcast_link(link_data) self.emane_manager.session.broadcast_link(link_data)

View file

@ -6,22 +6,25 @@ share the same MAC+PHY model.
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
from core.emulator.data import LinkData from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes,
LinkTypes, LinkTypes,
MessageFlags, MessageFlags,
NodeTypes, NodeTypes,
RegisterTlvs, RegisterTlvs,
TransportType,
) )
from core.nodes.base import CoreNetworkBase from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel
from core.emulator.session import Session from core.emulator.session import Session
from core.location.mobility import WirelessModel from core.location.mobility import WirelessModel, WayPointMobility
OptionalEmaneModel = Optional[EmaneModel]
WirelessModelType = Type[WirelessModel] WirelessModelType = Type[WirelessModel]
try: try:
@ -30,6 +33,7 @@ except ImportError:
try: try:
from emanesh.events import LocationEvent from emanesh.events import LocationEvent
except ImportError: except ImportError:
LocationEvent = None
logging.debug("compatible emane python bindings not installed") logging.debug("compatible emane python bindings not installed")
@ -40,67 +44,63 @@ class EmaneNet(CoreNetworkBase):
Emane controller object that exists in a session. Emane controller object that exists in a session.
""" """
apitype = NodeTypes.EMANE apitype: NodeTypes = NodeTypes.EMANE
linktype = LinkTypes.WIRED linktype: LinkTypes = LinkTypes.WIRED
type = "wlan" type: str = "wlan"
is_emane = True has_custom_iface: bool = True
def __init__( def __init__(
self, self,
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
start: bool = True,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
super().__init__(session, _id, name, start, server) super().__init__(session, _id, name, server)
self.conf = "" self.conf: str = ""
self.nemidmap = {} self.model: "OptionalEmaneModel" = None
self.model = None self.mobility: Optional[WayPointMobility] = None
self.mobility = None
def linkconfig( def linkconfig(
self, self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
) -> None: ) -> None:
""" """
The CommEffect model supports link configuration. The CommEffect model supports link configuration.
""" """
if not self.model: if not self.model:
return return
self.model.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2) self.model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None: def config(self, conf: str) -> None:
self.conf = conf self.conf = conf
def startup(self) -> None:
pass
def shutdown(self) -> None: def shutdown(self) -> None:
pass pass
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None: def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
raise CoreError("emane networks cannot be linked to other networks")
def updatemodel(self, config: Dict[str, str]) -> None: def updatemodel(self, config: Dict[str, str]) -> None:
if not self.model: if not self.model:
raise ValueError("no model set to update for node(%s)", self.id) raise CoreError(f"no model set to update for node({self.name})")
logging.info( logging.info(
"node(%s) updating model(%s): %s", self.id, self.model.name, config "node(%s) updating model(%s): %s", self.id, self.model.name, config
) )
self.model.set_configs(config, node_id=self.id) self.model.update_config(config)
def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None:
""" """
set the EmaneModel associated with this node set the EmaneModel associated with this node
""" """
logging.info("adding model: %s", model.name)
if model.config_type == RegisterTlvs.WIRELESS: if model.config_type == RegisterTlvs.WIRELESS:
# EmaneModel really uses values from ConfigurableManager # EmaneModel really uses values from ConfigurableManager
# when buildnemxml() is called, not during init() # when buildnemxml() is called, not during init()
@ -110,94 +110,21 @@ class EmaneNet(CoreNetworkBase):
self.mobility = model(session=self.session, _id=self.id) self.mobility = model(session=self.session, _id=self.id)
self.mobility.update_config(config) self.mobility.update_config(config)
def setnemid(self, netif: CoreInterface, nemid: int) -> None:
"""
Record an interface to numerical ID mapping. The Emane controller
object manages and assigns these IDs for all NEMs.
"""
self.nemidmap[netif] = nemid
def getnemid(self, netif: CoreInterface) -> Optional[int]:
"""
Given an interface, return its numerical ID.
"""
if netif not in self.nemidmap:
return None
else:
return self.nemidmap[netif]
def getnemnetif(self, nemid: int) -> Optional[CoreInterface]:
"""
Given a numerical NEM ID, return its interface. This returns the
first interface that matches the given NEM ID.
"""
for netif in self.nemidmap:
if self.nemidmap[netif] == nemid:
return netif
return None
def netifs(self, sort: bool = True) -> List[CoreInterface]:
"""
Retrieve list of linked interfaces sorted by node number.
"""
return sorted(self._netif.values(), key=lambda ifc: ifc.node.id)
def installnetifs(self) -> None:
"""
Install TAP devices into their namespaces. This is done after
EMANE daemons have been started, because that is their only chance
to bind to the TAPs.
"""
if (
self.session.emane.genlocationevents()
and self.session.emane.service is None
):
warntxt = "unable to publish EMANE events because the eventservice "
warntxt += "Python bindings failed to load"
logging.error(warntxt)
for netif in self.netifs():
external = self.session.emane.get_config(
"external", self.id, self.model.name
)
if external == "0":
netif.setaddrs()
if not self.session.emane.genlocationevents():
netif.poshook = None
continue
# at this point we register location handlers for generating
# EMANE location events
netif.poshook = self.setnemposition
netif.setposition()
def deinstallnetifs(self) -> None:
"""
Uninstall TAP devices. This invokes their shutdown method for
any required cleanup; the device may be actually removed when
emanetransportd terminates.
"""
for netif in self.netifs():
if netif.transport_type == TransportType.VIRTUAL:
netif.shutdown()
netif.poshook = None
def _nem_position( def _nem_position(
self, netif: CoreInterface self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, float]]: ) -> Optional[Tuple[int, float, float, float]]:
""" """
Creates nem position for emane event for a given interface. Creates nem position for emane event for a given interface.
:param netif: interface to get nem emane position for :param iface: interface to get nem emane position for
:return: nem position tuple, None otherwise :return: nem position tuple, None otherwise
""" """
nemid = self.getnemid(netif) nem_id = self.session.emane.get_nem_id(iface)
ifname = netif.localname ifname = iface.localname
if nemid is None: if nem_id is None:
logging.info("nemid for %s is unknown", ifname) logging.info("nemid for %s is unknown", ifname)
return return
node = netif.node node = iface.node
x, y, z = node.getposition() x, y, z = node.getposition()
lat, lon, alt = self.session.location.getgeo(x, y, z) lat, lon, alt = self.session.location.getgeo(x, y, z)
if node.position.alt is not None: if node.position.alt is not None:
@ -205,32 +132,31 @@ class EmaneNet(CoreNetworkBase):
node.position.set_geo(lon, lat, alt) node.position.set_geo(lon, lat, alt)
# altitude must be an integer or warning is printed # altitude must be an integer or warning is printed
alt = int(round(alt)) alt = int(round(alt))
return nemid, lon, lat, alt return nem_id, lon, lat, alt
def setnemposition(self, netif: CoreInterface) -> None: def setnemposition(self, iface: CoreInterface) -> None:
""" """
Publish a NEM location change event using the EMANE event service. Publish a NEM location change event using the EMANE event service.
:param netif: interface to set nem position for :param iface: interface to set nem position for
""" """
if self.session.emane.service is None: if self.session.emane.service is None:
logging.info("position service not available") logging.info("position service not available")
return return
position = self._nem_position(iface)
position = self._nem_position(netif)
if position: if position:
nemid, lon, lat, alt = position nemid, lon, lat, alt = position
event = LocationEvent() event = LocationEvent()
event.append(nemid, latitude=lat, longitude=lon, altitude=alt) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event) self.session.emane.service.publish(0, event)
def setnempositions(self, moved_netifs: List[CoreInterface]) -> None: def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
""" """
Several NEMs have moved, from e.g. a WaypointMobilityModel Several NEMs have moved, from e.g. a WaypointMobilityModel
calculation. Generate an EMANE Location Event having several calculation. Generate an EMANE Location Event having several
entries for each netif that has moved. entries for each interface that has moved.
""" """
if len(moved_netifs) == 0: if len(moved_ifaces) == 0:
return return
if self.session.emane.service is None: if self.session.emane.service is None:
@ -238,18 +164,21 @@ class EmaneNet(CoreNetworkBase):
return return
event = LocationEvent() event = LocationEvent()
for netif in moved_netifs: for iface in moved_ifaces:
position = self._nem_position(netif) position = self._nem_position(iface)
if position: if position:
nemid, lon, lat, alt = position nemid, lon, lat, alt = position
event.append(nemid, latitude=lat, longitude=lon, altitude=alt) event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event) self.session.emane.service.publish(0, event)
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().all_link_data(flags) links = super().links(flags)
# gather current emane links
nem_ids = set(self.nemidmap.values())
emane_manager = self.session.emane emane_manager = self.session.emane
# gather current emane links
nem_ids = set()
for iface in self.get_ifaces():
nem_id = emane_manager.get_nem_id(iface)
nem_ids.add(nem_id)
emane_links = emane_manager.link_monitor.links emane_links = emane_manager.link_monitor.links
considered = set() considered = set()
for link_key in emane_links: for link_key in emane_links:
@ -268,3 +197,18 @@ class EmaneNet(CoreNetworkBase):
if link: if link:
links.append(link) links.append(link)
return links return links
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface:
# TUN/TAP is not ready for addressing yet; the device may
# take some time to appear, and installing it into a
# namespace after it has been bound removes addressing;
# save addresses with the interface now
iface_id = node.newtuntap(iface_data.id, iface_data.name)
node.attachnet(iface_id, self)
iface = node.get_iface(iface_id)
iface.set_mac(iface_data.mac)
for ip in iface_data.get_ips():
iface.add_ip(ip)
if self.session.state == EventTypes.RUNTIME_STATE:
self.session.emane.start_iface(self, iface)
return iface

View file

@ -8,11 +8,11 @@ from core.emane import emanemodel
class EmaneRfPipeModel(emanemodel.EmaneModel): class EmaneRfPipeModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_rfpipe" name: str = "emane_rfpipe"
# mac configuration # mac configuration
mac_library = "rfpipemaclayer" mac_library: str = "rfpipemaclayer"
mac_xml = "rfpipemaclayer.xml" mac_xml: str = "rfpipemaclayer.xml"
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -4,6 +4,7 @@ tdma.py: EMANE TDMA model bindings for CORE
import logging import logging
import os import os
from typing import Set
from core import constants, utils from core import constants, utils
from core.config import Configuration from core.config import Configuration
@ -13,18 +14,18 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneTdmaModel(emanemodel.EmaneModel): class EmaneTdmaModel(emanemodel.EmaneModel):
# model name # model name
name = "emane_tdma" name: str = "emane_tdma"
# mac configuration # mac configuration
mac_library = "tdmaeventschedulerradiomodel" mac_library: str = "tdmaeventschedulerradiomodel"
mac_xml = "tdmaeventschedulerradiomodel.xml" mac_xml: str = "tdmaeventschedulerradiomodel.xml"
# add custom schedule options and ignore it when writing emane xml # add custom schedule options and ignore it when writing emane xml
schedule_name = "schedule" schedule_name: str = "schedule"
default_schedule = os.path.join( default_schedule: str = os.path.join(
constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml" constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml"
) )
config_ignore = {schedule_name} config_ignore: Set[str] = {schedule_name}
@classmethod @classmethod
def load(cls, emane_prefix: str) -> None: def load(cls, emane_prefix: str) -> None:

View file

@ -3,12 +3,13 @@ import logging
import os import os
import signal import signal
import sys import sys
from typing import Mapping, Type from typing import Dict, List, Type
import core.services import core.services
from core import configservices from core import configservices, utils
from core.configservice.manager import ConfigServiceManager from core.configservice.manager import ConfigServiceManager
from core.emulator.session import Session from core.emulator.session import Session
from core.executables import COMMON_REQUIREMENTS, OVS_REQUIREMENTS, VCMD_REQUIREMENTS
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
@ -36,7 +37,7 @@ class CoreEmu:
Provides logic for creating and configuring CORE sessions and the nodes within them. Provides logic for creating and configuring CORE sessions and the nodes within them.
""" """
def __init__(self, config: Mapping[str, str] = None) -> None: def __init__(self, config: Dict[str, str] = None) -> None:
""" """
Create a CoreEmu object. Create a CoreEmu object.
@ -48,27 +49,51 @@ class CoreEmu:
# configuration # configuration
if config is None: if config is None:
config = {} config = {}
self.config = config self.config: Dict[str, str] = config
# session management # session management
self.sessions = {} self.sessions: Dict[int, Session] = {}
# load services # load services
self.service_errors = [] self.service_errors: List[str] = []
self.load_services() self.load_services()
# config services # config services
self.service_manager = ConfigServiceManager() self.service_manager: ConfigServiceManager = ConfigServiceManager()
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
self.service_manager.load(config_services_path) self.service_manager.load(config_services_path)
custom_dir = self.config.get("custom_config_services_dir") custom_dir = self.config.get("custom_config_services_dir")
if custom_dir: if custom_dir:
self.service_manager.load(custom_dir) self.service_manager.load(custom_dir)
# check executables exist on path
self._validate_env()
# catch exit event # catch exit event
atexit.register(self.shutdown) atexit.register(self.shutdown)
def _validate_env(self) -> None:
"""
Validates executables CORE depends on exist on path.
:return: nothing
:raises core.errors.CoreError: when an executable does not exist on path
"""
requirements = COMMON_REQUIREMENTS
use_ovs = self.config.get("ovs") == "1"
if use_ovs:
requirements += OVS_REQUIREMENTS
else:
requirements += VCMD_REQUIREMENTS
for requirement in requirements:
utils.which(requirement, required=True)
def load_services(self) -> None: def load_services(self) -> None:
"""
Loads default and custom services for use within CORE.
:return: nothing
"""
# load default services # load default services
self.service_errors = core.services.load() self.service_errors = core.services.load()

View file

@ -1,18 +1,22 @@
""" """
CORE data objects. CORE data objects.
""" """
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass import netaddr
from typing import List, Tuple
from core import utils
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes, EventTypes,
ExceptionLevels, ExceptionLevels,
LinkTypes, LinkTypes,
MessageFlags, MessageFlags,
NodeTypes,
) )
if TYPE_CHECKING:
from core.nodes.base import CoreNode, NodeBase
@dataclass @dataclass
class ConfigData: class ConfigData:
@ -27,7 +31,7 @@ class ConfigData:
possible_values: str = None possible_values: str = None
groups: str = None groups: str = None
session: int = None session: int = None
interface_number: int = None iface_id: int = None
network_id: int = None network_id: int = None
opaque: str = None opaque: str = None
@ -68,65 +72,218 @@ class FileData:
@dataclass @dataclass
class NodeData: class NodeOptions:
message_type: MessageFlags = None """
id: int = None Options for creating and updating nodes within core.
node_type: NodeTypes = None """
name: str = None name: str = None
ip_address: str = None model: Optional[str] = "PC"
mac_address: str = None
ip6_address: str = None
model: str = None
emulation_id: int = None
server: str = None
session: int = None
x_position: float = None
y_position: float = None
canvas: int = None canvas: int = None
network_id: int = None
services: List[str] = None
latitude: float = None
longitude: float = None
altitude: float = None
icon: str = None icon: str = None
opaque: str = None services: List[str] = field(default_factory=list)
config_services: List[str] = field(default_factory=list)
x: float = None
y: float = None
lat: float = None
lon: float = None
alt: float = None
server: str = None
image: str = None
emane: str = None
def set_position(self, x: float, y: float) -> None:
"""
Convenience method for setting position.
:param x: x position
:param y: y position
:return: nothing
"""
self.x = x
self.y = y
def set_location(self, lat: float, lon: float, alt: float) -> None:
"""
Convenience method for setting location.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
"""
self.lat = lat
self.lon = lon
self.alt = alt
@dataclass
class NodeData:
"""
Node to broadcast.
"""
node: "NodeBase"
message_type: MessageFlags = None
source: str = None source: str = None
@dataclass
class InterfaceData:
"""
Convenience class for storing interface data.
"""
id: int = None
name: str = None
mac: str = None
ip4: str = None
ip4_mask: int = None
ip6: str = None
ip6_mask: int = None
def get_ips(self) -> List[str]:
"""
Returns a list of ip4 and ip6 addresses when present.
:return: list of ip addresses
"""
ips = []
if self.ip4 and self.ip4_mask:
ips.append(f"{self.ip4}/{self.ip4_mask}")
if self.ip6 and self.ip6_mask:
ips.append(f"{self.ip6}/{self.ip6_mask}")
return ips
@dataclass
class LinkOptions:
"""
Options for creating and updating links within core.
"""
delay: int = None
bandwidth: int = None
loss: float = None
dup: int = None
jitter: int = None
mer: int = None
burst: int = None
mburst: int = None
unidirectional: int = None
key: int = None
@dataclass @dataclass
class LinkData: class LinkData:
"""
Represents all data associated with a link.
"""
message_type: MessageFlags = None message_type: MessageFlags = None
type: LinkTypes = LinkTypes.WIRED
label: str = None label: str = None
node1_id: int = None node1_id: int = None
node2_id: int = None node2_id: int = None
delay: float = None
bandwidth: float = None
per: float = None
dup: float = None
jitter: float = None
mer: float = None
burst: float = None
session: int = None
mburst: float = None
link_type: LinkTypes = None
gui_attributes: str = None
unidirectional: int = None
emulation_id: int = None
network_id: int = None network_id: int = None
key: int = None iface1: InterfaceData = None
interface1_id: int = None iface2: InterfaceData = None
interface1_name: str = None options: LinkOptions = LinkOptions()
interface1_ip4: str = None
interface1_ip4_mask: int = None
interface1_mac: str = None
interface1_ip6: str = None
interface1_ip6_mask: int = None
interface2_id: int = None
interface2_name: str = None
interface2_ip4: str = None
interface2_ip4_mask: int = None
interface2_mac: str = None
interface2_ip6: str = None
interface2_ip6_mask: int = None
opaque: str = None
color: str = None color: str = None
source: str = None
class IpPrefixes:
"""
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
"""
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
"""
Creates an IpPrefixes object.
:param ip4_prefix: ip4 prefix to use for generation
:param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
"""
if not ip4_prefix and not ip6_prefix:
raise ValueError("ip4 or ip6 must be provided")
self.ip4 = None
if ip4_prefix:
self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None
if ip6_prefix:
self.ip6 = netaddr.IPNetwork(ip6_prefix)
def ip4_address(self, node_id: int) -> str:
"""
Convenience method to return the IP4 address for a node.
:param node_id: node id to get IP4 address for
:return: IP4 address or None
"""
if not self.ip4:
raise ValueError("ip4 prefixes have not been set")
return str(self.ip4[node_id])
def ip6_address(self, node_id: int) -> str:
"""
Convenience method to return the IP6 address for a node.
:param node_id: node id to get IP6 address for
:return: IP4 address or None
"""
if not self.ip6:
raise ValueError("ip6 prefixes have not been set")
return str(self.ip6[node_id])
def gen_iface(self, node_id: int, name: str = None, mac: str = None):
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node_id: node id to create an interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
# generate ip4 data
ip4 = None
ip4_mask = None
if self.ip4:
ip4 = self.ip4_address(node_id)
ip4_mask = self.ip4.prefixlen
# generate ip6 data
ip6 = None
ip6_mask = None
if self.ip6:
ip6 = self.ip6_address(node_id)
ip6_mask = self.ip6.prefixlen
# random mac
if not mac:
mac = utils.random_mac()
return InterfaceData(
name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
)
def create_iface(
self, node: "CoreNode", name: str = None, mac: str = None
) -> InterfaceData:
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node: node to create interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
iface_data = self.gen_iface(node.id, name, mac)
iface_data.id = node.next_iface_id()
return iface_data

View file

@ -37,10 +37,10 @@ class DistributedServer:
:param name: convenience name to associate with host :param name: convenience name to associate with host
:param host: host to connect to :param host: host to connect to
""" """
self.name = name self.name: str = name
self.host = host self.host: str = host
self.conn = Connection(host, user="root") self.conn: Connection = Connection(host, user="root")
self.lock = threading.Lock() self.lock: threading.Lock = threading.Lock()
def remote_cmd( def remote_cmd(
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True
@ -117,10 +117,10 @@ class DistributedController:
:param session: session :param session: session
""" """
self.session = session self.session: "Session" = session
self.servers = OrderedDict() self.servers: Dict[str, DistributedServer] = OrderedDict()
self.tunnels = {} self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
self.address = self.session.options.get_config( self.address: str = self.session.options.get_config(
"distributed_address", default=None "distributed_address", default=None
) )
@ -178,13 +178,10 @@ class DistributedController:
""" """
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork): if not isinstance(node, CoreNetwork):
continue continue
if isinstance(node, CtrlNet) and node.serverintf is not None: if isinstance(node, CtrlNet) and node.serverintf is not None:
continue continue
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
self.create_gre_tunnel(node, server) self.create_gre_tunnel(node, server)
@ -195,7 +192,6 @@ class DistributedController:
""" """
Create gre tunnel using a pair of gre taps between the local and remote server. Create gre tunnel using a pair of gre taps between the local and remote server.
:param node: node to create gre tunnel for :param node: node to create gre tunnel for
:param server: server to create :param server: server to create
tunnel for tunnel for
@ -212,7 +208,7 @@ class DistributedController:
"local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
) )
local_tap = GreTap(session=self.session, remoteip=host, key=key) local_tap = GreTap(session=self.session, remoteip=host, key=key)
local_tap.net_client.set_interface_master(node.brname, local_tap.localname) local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
# server to local # server to local
logging.info( logging.info(
@ -221,37 +217,27 @@ class DistributedController:
remote_tap = GreTap( remote_tap = GreTap(
session=self.session, remoteip=self.address, key=key, server=server session=self.session, remoteip=self.address, key=key, server=server
) )
remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname) remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
# save tunnels for shutdown # save tunnels for shutdown
tunnel = (local_tap, remote_tap) tunnel = (local_tap, remote_tap)
self.tunnels[key] = tunnel self.tunnels[key] = tunnel
return tunnel return tunnel
def tunnel_key(self, n1_id: int, n2_id: int) -> int: def tunnel_key(self, node1_id: int, node2_id: int) -> int:
""" """
Compute a 32-bit key used to uniquely identify a GRE tunnel. Compute a 32-bit key used to uniquely identify a GRE tunnel.
The hash(n1num), hash(n2num) values are used, so node numbers may be The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet"). None or string values (used for e.g. "ctrlnet").
:param n1_id: node one id :param node1_id: node one id
:param n2_id: node two id :param node2_id: node two id
:return: tunnel key for the node pair :return: tunnel key for the node pair
""" """
logging.debug("creating tunnel key for: %s, %s", n1_id, n2_id) logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id)
key = ( key = (
(self.session.id << 16) ^ utils.hashkey(n1_id) ^ (utils.hashkey(n2_id) << 8) (self.session.id << 16)
^ utils.hashkey(node1_id)
^ (utils.hashkey(node2_id) << 8)
) )
return key & 0xFFFFFFFF return key & 0xFFFFFFFF
def get_tunnel(self, n1_id: int, n2_id: int) -> GreTap:
"""
Return the GreTap between two nodes if it exists.
:param n1_id: node one id
:param n2_id: node two id
:return: gre tap between nodes or None
"""
key = self.tunnel_key(n1_id, n2_id)
logging.debug("checking for tunnel key(%s) in: %s", key, self.tunnels)
return self.tunnels.get(key)

View file

@ -1,325 +0,0 @@
from typing import List, Optional, Union
import netaddr
from core import utils
from core.api.grpc.core_pb2 import LinkOptions
from core.emulator.enumerations import LinkTypes
from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface
from core.nodes.physical import PhysicalNode
class IdGen:
def __init__(self, _id: int = 0) -> None:
self.id = _id
def next(self) -> int:
self.id += 1
return self.id
def link_config(
node: Union[CoreNetworkBase, PhysicalNode],
interface: CoreInterface,
link_options: LinkOptions,
interface_two: CoreInterface = None,
) -> None:
"""
Convenience method for configuring a link,
:param node: network to configure link for
:param interface: interface to configure
:param link_options: data to configure link with
:param interface_two: other interface associated, default is None
:return: nothing
"""
node.linkconfig(
interface,
link_options.bandwidth,
link_options.delay,
link_options.per,
link_options.dup,
link_options.jitter,
interface_two,
)
class NodeOptions:
"""
Options for creating and updating nodes within core.
"""
def __init__(self, name: str = None, model: str = "PC", image: str = None) -> None:
"""
Create a NodeOptions object.
:param name: name of node, defaults to node class name postfix with its id
:param model: defines services for default and physical nodes, defaults to
"router"
:param image: image to use for docker nodes
"""
self.name = name
self.model = model
self.canvas = None
self.icon = None
self.opaque = None
self.services = []
self.config_services = []
self.x = None
self.y = None
self.lat = None
self.lon = None
self.alt = None
self.emulation_id = None
self.server = None
self.image = image
self.emane = None
def set_position(self, x: float, y: float) -> None:
"""
Convenience method for setting position.
:param x: x position
:param y: y position
:return: nothing
"""
self.x = x
self.y = y
def set_location(self, lat: float, lon: float, alt: float) -> None:
"""
Convenience method for setting location.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
"""
self.lat = lat
self.lon = lon
self.alt = alt
class LinkOptions:
"""
Options for creating and updating links within core.
"""
def __init__(self, _type: LinkTypes = LinkTypes.WIRED) -> None:
"""
Create a LinkOptions object.
:param _type: type of link, defaults to
wired
"""
self.type = _type
self.session = None
self.delay = None
self.bandwidth = None
self.per = None
self.dup = None
self.jitter = None
self.mer = None
self.burst = None
self.mburst = None
self.gui_attributes = None
self.unidirectional = None
self.emulation_id = None
self.network_id = None
self.key = None
self.opaque = None
class InterfaceData:
"""
Convenience class for storing interface data.
"""
def __init__(
self,
_id: int,
name: str,
mac: str,
ip4: str,
ip4_mask: int,
ip6: str,
ip6_mask: int,
) -> None:
"""
Creates an InterfaceData object.
:param _id: interface id
:param name: name for interface
:param mac: mac address
:param ip4: ipv4 address
:param ip4_mask: ipv4 bit mask
:param ip6: ipv6 address
:param ip6_mask: ipv6 bit mask
"""
self.id = _id
self.name = name
self.mac = mac
self.ip4 = ip4
self.ip4_mask = ip4_mask
self.ip6 = ip6
self.ip6_mask = ip6_mask
def has_ip4(self) -> bool:
"""
Determines if interface has an ip4 address.
:return: True if has ip4, False otherwise
"""
return all([self.ip4, self.ip4_mask])
def has_ip6(self) -> bool:
"""
Determines if interface has an ip6 address.
:return: True if has ip6, False otherwise
"""
return all([self.ip6, self.ip6_mask])
def ip4_address(self) -> Optional[str]:
"""
Retrieve a string representation of the ip4 address and netmask.
:return: ip4 string or None
"""
if self.has_ip4():
return f"{self.ip4}/{self.ip4_mask}"
else:
return None
def ip6_address(self) -> Optional[str]:
"""
Retrieve a string representation of the ip6 address and netmask.
:return: ip4 string or None
"""
if self.has_ip6():
return f"{self.ip6}/{self.ip6_mask}"
else:
return None
def get_addresses(self) -> List[str]:
"""
Returns a list of ip4 and ip6 address when present.
:return: list of addresses
"""
ip4 = self.ip4_address()
ip6 = self.ip6_address()
return [i for i in [ip4, ip6] if i]
class IpPrefixes:
"""
Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
"""
def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
"""
Creates an IpPrefixes object.
:param ip4_prefix: ip4 prefix to use for generation
:param ip6_prefix: ip6 prefix to use for generation
:raises ValueError: when both ip4 and ip6 prefixes have not been provided
"""
if not ip4_prefix and not ip6_prefix:
raise ValueError("ip4 or ip6 must be provided")
self.ip4 = None
if ip4_prefix:
self.ip4 = netaddr.IPNetwork(ip4_prefix)
self.ip6 = None
if ip6_prefix:
self.ip6 = netaddr.IPNetwork(ip6_prefix)
def ip4_address(self, node: CoreNode) -> str:
"""
Convenience method to return the IP4 address for a node.
:param node: node to get IP4 address for
:return: IP4 address or None
"""
if not self.ip4:
raise ValueError("ip4 prefixes have not been set")
return str(self.ip4[node.id])
def ip6_address(self, node: CoreNode) -> str:
"""
Convenience method to return the IP6 address for a node.
:param node: node to get IP6 address for
:return: IP4 address or None
"""
if not self.ip6:
raise ValueError("ip6 prefixes have not been set")
return str(self.ip6[node.id])
def create_interface(
self, node: CoreNode, name: str = None, mac: str = None
) -> InterfaceData:
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
:param node: node to create interface for
:param name: name to set for interface, default is eth{id}
:param mac: mac address to use for this interface, default is random
generation
:return: new interface data for the provided node
"""
# interface id
inteface_id = node.newifindex()
# generate ip4 data
ip4 = None
ip4_mask = None
if self.ip4:
ip4 = self.ip4_address(node)
ip4_mask = self.ip4.prefixlen
# generate ip6 data
ip6 = None
ip6_mask = None
if self.ip6:
ip6 = self.ip6_address(node)
ip6_mask = self.ip6.prefixlen
# random mac
if not mac:
mac = utils.random_mac()
return InterfaceData(
_id=inteface_id,
name=name,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
mac=mac,
)
def create_interface(
node: CoreNode, network: CoreNetworkBase, interface_data: InterfaceData
):
"""
Create an interface for a node on a network using provided interface data.
:param node: node to create interface for
:param network: network to associate interface with
:param interface_data: interface data
:return: created interface
"""
node.newnetif(
network,
addrlist=interface_data.get_addresses(),
hwaddr=interface_data.mac,
ifindex=interface_data.id,
ifname=interface_data.name,
)
return node.netif(interface_data.id)

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
from typing import Any from typing import Any, List
from core.config import ConfigurableManager, ConfigurableOptions, Configuration from core.config import ConfigurableManager, ConfigurableOptions, Configuration
from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs
@ -10,8 +10,8 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
Provides session configuration. Provides session configuration.
""" """
name = "session" name: str = "session"
options = [ options: List[Configuration] = [
Configuration( Configuration(
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network" _id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
), ),
@ -56,8 +56,11 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
default=Sdt.DEFAULT_SDT_URL, default=Sdt.DEFAULT_SDT_URL,
label="SDT3D URL", label="SDT3D URL",
), ),
Configuration(
_id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS"
),
] ]
config_type = RegisterTlvs.UTILITY config_type: RegisterTlvs = RegisterTlvs.UTILITY
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()

View file

@ -0,0 +1,16 @@
from typing import List
VNODED: str = "vnoded"
VCMD: str = "vcmd"
SYSCTL: str = "sysctl"
IP: str = "ip"
ETHTOOL: str = "ethtool"
TC: str = "tc"
EBTABLES: str = "ebtables"
MOUNT: str = "mount"
UMOUNT: str = "umount"
OVS_VSCTL: str = "ovs-vsctl"
COMMON_REQUIREMENTS: List[str] = [SYSCTL, IP, ETHTOOL, TC, EBTABLES, MOUNT, UMOUNT]
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]

View file

@ -3,21 +3,26 @@ import math
import tkinter as tk import tkinter as tk
from tkinter import PhotoImage, font, ttk from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type
import grpc import grpc
from core.gui import appconfig, themes from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient from core.gui.coreclient import CoreClient
from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.error import ErrorDialog
from core.gui.frames.base import InfoFrameBase
from core.gui.frames.default import DefaultInfoFrame
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.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.themes import PADY
from core.gui.toolbar import Toolbar from core.gui.toolbar import Toolbar
WIDTH = 1000 WIDTH: int = 1000
HEIGHT = 800 HEIGHT: int = 800
class Application(ttk.Frame): class Application(ttk.Frame):
@ -27,25 +32,28 @@ class Application(ttk.Frame):
NodeUtils.setup() NodeUtils.setup()
# widgets # widgets
self.menubar = None self.menubar: Optional[Menubar] = None
self.toolbar = None self.toolbar: Optional[Toolbar] = None
self.right_frame = None self.right_frame: Optional[ttk.Frame] = None
self.canvas = None self.canvas: Optional[CanvasGraph] = None
self.statusbar = None self.statusbar: Optional[StatusBar] = None
self.progress = None self.progress: Optional[Progressbar] = None
self.infobar: Optional[ttk.Frame] = None
self.info_frame: Optional[InfoFrameBase] = None
self.show_infobar: tk.BooleanVar = tk.BooleanVar(value=False)
# fonts # fonts
self.fonts_size = None self.fonts_size: Dict[str, int] = {}
self.icon_text_font = None self.icon_text_font: Optional[font.Font] = None
self.edge_font = None self.edge_font: Optional[font.Font] = None
# setup # setup
self.guiconfig = appconfig.read() self.guiconfig: GuiConfig = appconfig.read()
self.app_scale = self.guiconfig.scale self.app_scale: float = self.guiconfig.scale
self.setup_scaling() self.setup_scaling()
self.style = ttk.Style() self.style: ttk.Style = ttk.Style()
self.setup_theme() self.setup_theme()
self.core = CoreClient(self, proxy) self.core: CoreClient = CoreClient(self, proxy)
self.setup_app() self.setup_app()
self.draw() self.draw()
self.core.setup() self.core.setup()
@ -111,16 +119,27 @@ class Application(ttk.Frame):
self.right_frame.rowconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew") self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas() self.draw_canvas()
self.draw_infobar()
self.draw_status() self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate") self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self) self.menubar = Menubar(self)
self.master.config(menu=self.menubar) self.master.config(menu=self.menubar)
def draw_infobar(self) -> None:
self.infobar = ttk.Frame(self.right_frame, padding=5, relief=tk.RAISED)
self.infobar.columnconfigure(0, weight=1)
self.infobar.rowconfigure(1, weight=1)
label_font = font.Font(weight=font.BOLD, underline=tk.TRUE)
label = ttk.Label(
self.infobar, text="Details", anchor=tk.CENTER, font=label_font
)
label.grid(sticky=tk.EW, pady=PADY)
def draw_canvas(self) -> None: def draw_canvas(self) -> None:
canvas_frame = ttk.Frame(self.right_frame) canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1) canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(sticky="nsew", pady=1) canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core) self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew") self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview) scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -134,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None: def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew") self.statusbar.grid(sticky="ew", columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get():
return
self.clear_info()
self.info_frame = frame_class(self.infobar, **kwargs)
self.info_frame.draw()
self.info_frame.grid(sticky="nsew")
def clear_info(self) -> None:
if self.info_frame:
self.info_frame.destroy()
self.info_frame = None
def default_info(self) -> None:
self.clear_info()
self.display_info(DefaultInfoFrame, app=self)
def show_info(self) -> None:
self.default_info()
self.infobar.grid(row=0, column=1, sticky="nsew")
def hide_info(self) -> None:
self.infobar.grid_forget()
def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None: def show_grpc_exception(self, title: str, e: grpc.RpcError) -> None:
logging.exception("app grpc exception", exc_info=e) logging.exception("app grpc exception", exc_info=e)

View file

@ -1,32 +1,32 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import Dict, List, Optional, Type
import yaml import yaml
from core.gui import themes from core.gui import themes
HOME_PATH = Path.home().joinpath(".coregui") HOME_PATH: Path = Path.home().joinpath(".coregui")
BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") BACKGROUNDS_PATH: Path = HOME_PATH.joinpath("backgrounds")
CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_EMANE_PATH: Path = HOME_PATH.joinpath("custom_emane")
CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") CUSTOM_SERVICE_PATH: Path = HOME_PATH.joinpath("custom_services")
ICONS_PATH = HOME_PATH.joinpath("icons") ICONS_PATH: Path = HOME_PATH.joinpath("icons")
MOBILITY_PATH = HOME_PATH.joinpath("mobility") MOBILITY_PATH: Path = HOME_PATH.joinpath("mobility")
XMLS_PATH = HOME_PATH.joinpath("xmls") XMLS_PATH: Path = HOME_PATH.joinpath("xmls")
CONFIG_PATH = HOME_PATH.joinpath("config.yaml") CONFIG_PATH: Path = HOME_PATH.joinpath("config.yaml")
LOG_PATH = HOME_PATH.joinpath("gui.log") LOG_PATH: Path = HOME_PATH.joinpath("gui.log")
SCRIPT_PATH = HOME_PATH.joinpath("scripts") SCRIPT_PATH: Path = HOME_PATH.joinpath("scripts")
# local paths # local paths
DATA_PATH = Path(__file__).parent.joinpath("data") DATA_PATH: Path = Path(__file__).parent.joinpath("data")
LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() LOCAL_ICONS_PATH: Path = DATA_PATH.joinpath("icons").absolute()
LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() LOCAL_BACKGROUND_PATH: Path = DATA_PATH.joinpath("backgrounds").absolute()
LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() LOCAL_XMLS_PATH: Path = DATA_PATH.joinpath("xmls").absolute()
LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() LOCAL_MOBILITY_PATH: Path = DATA_PATH.joinpath("mobility").absolute()
# configuration data # configuration data
TERMINALS = { TERMINALS: Dict[str, str] = {
"xterm": "xterm -e", "xterm": "xterm -e",
"aterm": "aterm -e", "aterm": "aterm -e",
"eterm": "eterm -e", "eterm": "eterm -e",
@ -36,45 +36,45 @@ TERMINALS = {
"xfce4-terminal": "xfce4-terminal -x", "xfce4-terminal": "xfce4-terminal -x",
"gnome-terminal": "gnome-terminal --window --", "gnome-terminal": "gnome-terminal --window --",
} }
EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] EDITORS: List[str] = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
class IndentDumper(yaml.Dumper): class IndentDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False): def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
return super().increase_indent(flow, False) super().increase_indent(flow, False)
class CustomNode(yaml.YAMLObject): class CustomNode(yaml.YAMLObject):
yaml_tag = "!CustomNode" yaml_tag: str = "!CustomNode"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, image: str, services: List[str]) -> None: def __init__(self, name: str, image: str, services: List[str]) -> None:
self.name = name self.name: str = name
self.image = image self.image: str = image
self.services = services self.services: List[str] = services
class CoreServer(yaml.YAMLObject): class CoreServer(yaml.YAMLObject):
yaml_tag = "!CoreServer" yaml_tag: str = "!CoreServer"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, address: str) -> None: def __init__(self, name: str, address: str) -> None:
self.name = name self.name: str = name
self.address = address self.address: str = address
class Observer(yaml.YAMLObject): class Observer(yaml.YAMLObject):
yaml_tag = "!Observer" yaml_tag: str = "!Observer"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__(self, name: str, cmd: str) -> None: def __init__(self, name: str, cmd: str) -> None:
self.name = name self.name: str = name
self.cmd = cmd self.cmd: str = cmd
class PreferencesConfig(yaml.YAMLObject): class PreferencesConfig(yaml.YAMLObject):
yaml_tag = "!PreferencesConfig" yaml_tag: str = "!PreferencesConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -85,17 +85,17 @@ class PreferencesConfig(yaml.YAMLObject):
width: int = 1000, width: int = 1000,
height: int = 750, height: int = 750,
) -> None: ) -> None:
self.theme = theme self.theme: str = theme
self.editor = editor self.editor: str = editor
self.terminal = terminal self.terminal: str = terminal
self.gui3d = gui3d self.gui3d: str = gui3d
self.width = width self.width: int = width
self.height = height self.height: int = height
class LocationConfig(yaml.YAMLObject): class LocationConfig(yaml.YAMLObject):
yaml_tag = "!LocationConfig" yaml_tag: str = "!LocationConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -107,18 +107,18 @@ class LocationConfig(yaml.YAMLObject):
alt: float = 2.0, alt: float = 2.0,
scale: float = 150.0, scale: float = 150.0,
) -> None: ) -> None:
self.x = x self.x: float = x
self.y = y self.y: float = y
self.z = z self.z: float = z
self.lat = lat self.lat: float = lat
self.lon = lon self.lon: float = lon
self.alt = alt self.alt: float = alt
self.scale = scale self.scale: float = scale
class IpConfigs(yaml.YAMLObject): class IpConfigs(yaml.YAMLObject):
yaml_tag = "!IpConfigs" yaml_tag: str = "!IpConfigs"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -129,21 +129,21 @@ class IpConfigs(yaml.YAMLObject):
) -> None: ) -> None:
if ip4s is None: if ip4s is None:
ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"]
self.ip4s = ip4s self.ip4s: List[str] = ip4s
if ip6s is None: if ip6s is None:
ip6s = ["2001::", "2002::", "a::"] ip6s = ["2001::", "2002::", "a::"]
self.ip6s = ip6s self.ip6s: List[str] = ip6s
if ip4 is None: if ip4 is None:
ip4 = self.ip4s[0] ip4 = self.ip4s[0]
self.ip4 = ip4 self.ip4: str = ip4
if ip6 is None: if ip6 is None:
ip6 = self.ip6s[0] ip6 = self.ip6s[0]
self.ip6 = ip6 self.ip6: str = ip6
class GuiConfig(yaml.YAMLObject): class GuiConfig(yaml.YAMLObject):
yaml_tag = "!GuiConfig" yaml_tag: str = "!GuiConfig"
yaml_loader = yaml.SafeLoader yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader
def __init__( def __init__(
self, self,
@ -159,30 +159,30 @@ class GuiConfig(yaml.YAMLObject):
) -> None: ) -> None:
if preferences is None: if preferences is None:
preferences = PreferencesConfig() preferences = PreferencesConfig()
self.preferences = preferences self.preferences: PreferencesConfig = preferences
if location is None: if location is None:
location = LocationConfig() location = LocationConfig()
self.location = location self.location: LocationConfig = location
if servers is None: if servers is None:
servers = [] servers = []
self.servers = servers self.servers: List[CoreServer] = servers
if nodes is None: if nodes is None:
nodes = [] nodes = []
self.nodes = nodes self.nodes: List[CustomNode] = nodes
if recentfiles is None: if recentfiles is None:
recentfiles = [] recentfiles = []
self.recentfiles = recentfiles self.recentfiles: List[str] = recentfiles
if observers is None: if observers is None:
observers = [] observers = []
self.observers = observers self.observers: List[Observer] = observers
self.scale = scale self.scale: float = scale
if ips is None: if ips is None:
ips = IpConfigs() ips = IpConfigs()
self.ips = ips self.ips: IpConfigs = ips
self.mac = mac self.mac: str = mac
def copy_files(current_path, new_path) -> None: def copy_files(current_path: Path, new_path: Path) -> None:
for current_file in current_path.glob("*"): for current_file in current_path.glob("*"):
new_file = new_path.joinpath(current_file.name) new_file = new_path.joinpath(current_file.name)
shutil.copy(current_file, new_file) shutil.copy(current_file, new_file)

View file

@ -4,18 +4,41 @@ Incorporate grpc into python tkinter GUI
import json import json
import logging import logging
import os import os
import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import messagebox from tkinter import messagebox
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
import grpc import grpc
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2 from core.api.grpc import client
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.configservices_pb2 import ConfigService, ConfigServiceConfig
from core.api.grpc.core_pb2 import (
Event,
ExceptionEvent,
Hook,
Interface,
Link,
LinkEvent,
LinkType,
MessageType,
Node,
NodeEvent,
NodeType,
Position,
SessionLocation,
SessionState,
StartSessionResponse,
StopSessionResponse,
ThroughputsEvent,
)
from core.api.grpc.emane_pb2 import EmaneModelConfig from core.api.grpc.emane_pb2 import EmaneModelConfig
from core.api.grpc.mobility_pb2 import MobilityConfig 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.appconfig import CoreServer, Observer
from core.gui.dialogs.emaneinstall import EmaneInstallDialog from core.gui.dialogs.emaneinstall import EmaneInstallDialog
from core.gui.dialogs.error import ErrorDialog from core.gui.dialogs.error import ErrorDialog
from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.mobilityplayer import MobilityPlayer
@ -34,47 +57,46 @@ GUI_SOURCE = "gui"
class CoreClient: class CoreClient:
def __init__(self, app: "Application", proxy: bool): def __init__(self, app: "Application", proxy: bool) -> None:
""" """
Create a CoreGrpc instance Create a CoreGrpc instance
""" """
self._client = client.CoreGrpcClient(proxy=proxy) self.app: "Application" = app
self.session_id = None self.master: tk.Tk = app.master
self.node_ids = [] self._client: client.CoreGrpcClient = client.CoreGrpcClient(proxy=proxy)
self.app = app self.session_id: Optional[int] = None
self.master = app.master self.services: Dict[str, Set[str]] = {}
self.services = {} self.config_services_groups: Dict[str, Set[str]] = {}
self.config_services_groups = {} self.config_services: Dict[str, ConfigService] = {}
self.config_services = {} self.default_services: Dict[NodeType, Set[str]] = {}
self.default_services = {} self.emane_models: List[str] = []
self.emane_models = [] self.observer: Optional[str] = None
self.observer = None
# loaded configuration data # loaded configuration data
self.servers = {} self.servers: Dict[str, CoreServer] = {}
self.custom_nodes = {} self.custom_nodes: Dict[str, NodeDraw] = {}
self.custom_observers = {} self.custom_observers: Dict[str, Observer] = {}
self.read_config() self.read_config()
# helpers # helpers
self.interface_to_edge = {} self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {}
self.interfaces_manager = InterfaceManager(self.app) self.ifaces_manager: InterfaceManager = InterfaceManager(self.app)
# session data # session data
self.state = None self.state: Optional[SessionState] = None
self.canvas_nodes = {} self.canvas_nodes: Dict[int, CanvasNode] = {}
self.location = None self.location: Optional[SessionLocation] = None
self.links = {} self.links: Dict[Tuple[int, int], CanvasEdge] = {}
self.hooks = {} self.hooks: Dict[str, Hook] = {}
self.emane_config = None self.emane_config: Dict[str, ConfigOption] = {}
self.mobility_players = {} self.mobility_players: Dict[int, MobilityPlayer] = {}
self.handling_throughputs = None self.handling_throughputs: Optional[grpc.Channel] = None
self.handling_events = None self.handling_events: Optional[grpc.Channel] = None
self.xml_dir = None self.xml_dir: Optional[str] = None
self.xml_file = None self.xml_file: Optional[str] = None
@property @property
def client(self): def client(self) -> client.CoreGrpcClient:
if self.session_id: if self.session_id:
response = self._client.check_session(self.session_id) response = self._client.check_session(self.session_id)
if not response.result: if not response.result:
@ -89,10 +111,10 @@ class CoreClient:
self.enable_throughputs() self.enable_throughputs()
return self._client return self._client
def reset(self): def reset(self) -> None:
# helpers # helpers
self.interfaces_manager.reset() self.ifaces_manager.reset()
self.interface_to_edge.clear() self.iface_to_edge.clear()
# session data # session data
self.canvas_nodes.clear() self.canvas_nodes.clear()
self.links.clear() self.links.clear()
@ -104,14 +126,14 @@ class CoreClient:
self.cancel_throughputs() self.cancel_throughputs()
self.cancel_events() self.cancel_events()
def close_mobility_players(self): def close_mobility_players(self) -> None:
for mobility_player in self.mobility_players.values(): for mobility_player in self.mobility_players.values():
mobility_player.close() mobility_player.close()
def set_observer(self, value: str): def set_observer(self, value: Optional[str]) -> None:
self.observer = value self.observer = value
def read_config(self): def read_config(self) -> None:
# read distributed servers # read distributed servers
for server in self.app.guiconfig.servers: for server in self.app.guiconfig.servers:
self.servers[server.name] = server self.servers[server.name] = server
@ -125,7 +147,9 @@ class CoreClient:
for observer in self.app.guiconfig.observers: for observer in self.app.guiconfig.observers:
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: Event) -> None:
if event.source == GUI_SOURCE:
return
if event.session_id != self.session_id: if event.session_id != self.session_id:
logging.warning( logging.warning(
"ignoring event session(%s) current(%s)", "ignoring event session(%s) current(%s)",
@ -139,7 +163,7 @@ class CoreClient:
elif event.HasField("session_event"): elif event.HasField("session_event"):
logging.info("session event: %s", event) logging.info("session event: %s", event)
session_event = event.session_event session_event = event.session_event
if session_event.event <= core_pb2.SessionState.SHUTDOWN: if session_event.event <= SessionState.SHUTDOWN:
self.state = event.session_event.event self.state = event.session_event.event
elif session_event.event in {7, 8, 9}: elif session_event.event in {7, 8, 9}:
node_id = session_event.node_id node_id = session_event.node_id
@ -162,56 +186,77 @@ class CoreClient:
else: else:
logging.info("unhandled event: %s", event) logging.info("unhandled event: %s", event)
def handle_link_event(self, event: core_pb2.LinkEvent): def handle_link_event(self, event: LinkEvent) -> None:
logging.debug("Link event: %s", event) logging.debug("Link event: %s", event)
node_one_id = event.link.node_one_id node1_id = event.link.node1_id
node_two_id = event.link.node_two_id node2_id = event.link.node2_id
if node_one_id == node_two_id: if node1_id == node2_id:
logging.warning("ignoring links with loops: %s", event) logging.warning("ignoring links with loops: %s", event)
return return
canvas_node_one = self.canvas_nodes[node_one_id] canvas_node1 = self.canvas_nodes[node1_id]
canvas_node_two = self.canvas_nodes[node_two_id] canvas_node2 = self.canvas_nodes[node2_id]
if event.message_type == core_pb2.MessageType.ADD: if event.link.type == LinkType.WIRELESS:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wireless_edge( self.app.canvas.add_wireless_edge(
canvas_node_one, canvas_node_two, event.link canvas_node1, canvas_node2, event.link
) )
elif event.message_type == core_pb2.MessageType.DELETE: elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wireless_edge( self.app.canvas.delete_wireless_edge(
canvas_node_one, canvas_node_two, event.link canvas_node1, canvas_node2, event.link
) )
elif event.message_type == core_pb2.MessageType.NONE: elif event.message_type == MessageType.NONE:
self.app.canvas.update_wireless_edge( self.app.canvas.update_wireless_edge(
canvas_node_one, canvas_node_two, event.link canvas_node1, canvas_node2, event.link
)
else:
logging.warning("unknown link event: %s", event)
else:
if event.message_type == MessageType.ADD:
self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link)
self.app.canvas.organize()
elif event.message_type == MessageType.DELETE:
self.app.canvas.delete_wired_edge(canvas_node1, canvas_node2)
elif event.message_type == MessageType.NONE:
self.app.canvas.update_wired_edge(
canvas_node1, canvas_node2, event.link
) )
else: else:
logging.warning("unknown link event: %s", event) logging.warning("unknown link event: %s", event)
def handle_node_event(self, event: core_pb2.NodeEvent): def handle_node_event(self, event: NodeEvent) -> None:
logging.debug("node event: %s", event) logging.debug("node event: %s", event)
if event.source == GUI_SOURCE: if event.message_type == MessageType.NONE:
return canvas_node = self.canvas_nodes[event.node.id]
node_id = event.node.id
x = event.node.position.x x = event.node.position.x
y = event.node.position.y y = event.node.position.y
canvas_node = self.canvas_nodes[node_id]
canvas_node.move(x, y) canvas_node.move(x, y)
elif event.message_type == MessageType.DELETE:
canvas_node = self.canvas_nodes[event.node.id]
self.app.canvas.clear_selection()
self.app.canvas.select_object(canvas_node.id)
self.app.canvas.delete_selected_objects()
elif event.message_type == MessageType.ADD:
self.app.canvas.add_core_node(event.node)
else:
logging.warning("unknown node event: %s", event)
def enable_throughputs(self): def enable_throughputs(self) -> None:
self.handling_throughputs = self.client.throughputs( self.handling_throughputs = self.client.throughputs(
self.session_id, self.handle_throughputs self.session_id, self.handle_throughputs
) )
def cancel_throughputs(self): def cancel_throughputs(self) -> None:
if self.handling_throughputs: if self.handling_throughputs:
self.handling_throughputs.cancel() self.handling_throughputs.cancel()
self.handling_throughputs = None self.handling_throughputs = None
self.app.canvas.clear_throughputs()
def cancel_events(self): def cancel_events(self) -> None:
if self.handling_events: if self.handling_events:
self.handling_events.cancel() self.handling_events.cancel()
self.handling_events = None self.handling_events = None
def handle_throughputs(self, event: core_pb2.ThroughputsEvent): def handle_throughputs(self, event: ThroughputsEvent) -> None:
if event.session_id != self.session_id: if event.session_id != self.session_id:
logging.warning( logging.warning(
"ignoring throughput event session(%s) current(%s)", "ignoring throughput event session(%s) current(%s)",
@ -222,11 +267,11 @@ class CoreClient:
logging.debug("handling throughputs event: %s", event) logging.debug("handling throughputs event: %s", event)
self.app.after(0, self.app.canvas.set_throughputs, event) self.app.after(0, self.app.canvas.set_throughputs, event)
def handle_exception_event(self, event: core_pb2.ExceptionEvent): def handle_exception_event(self, event: ExceptionEvent) -> None:
logging.info("exception event: %s", event) logging.info("exception event: %s", event)
self.app.statusbar.core_alarms.append(event) self.app.statusbar.core_alarms.append(event)
def join_session(self, session_id: int, query_location: bool = True): def join_session(self, session_id: int, query_location: bool = True) -> None:
logging.info("join session(%s)", session_id) logging.info("join session(%s)", session_id)
# update session and title # update session and title
self.session_id = session_id self.session_id = session_id
@ -269,7 +314,7 @@ class CoreClient:
self.emane_config = response.config self.emane_config = response.config
# update interface manager # update interface manager
self.interfaces_manager.joined(session.links) self.ifaces_manager.joined(session.links)
# draw session # draw session
self.app.canvas.reset_and_redraw(session) self.app.canvas.reset_and_redraw(session)
@ -284,11 +329,11 @@ class CoreClient:
# 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 iface_id = None
if config.interface != -1: if config.iface_id != -1:
interface = config.interface iface_id = config.iface_id
canvas_node = self.canvas_nodes[config.node_id] canvas_node = self.canvas_nodes[config.node_id]
canvas_node.emane_model_configs[(config.model, interface)] = dict( canvas_node.emane_model_configs[(config.model, iface_id)] = dict(
config.config config.config
) )
@ -332,14 +377,14 @@ class CoreClient:
# organize canvas # organize canvas
self.app.canvas.organize() self.app.canvas.organize()
self.show_mobility_players()
# 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)
def is_runtime(self) -> bool: def is_runtime(self) -> bool:
return self.state == core_pb2.SessionState.RUNTIME return self.state == SessionState.RUNTIME
def parse_metadata(self, config: Dict[str, str]): def parse_metadata(self, config: Dict[str, str]) -> None:
# canvas setting # canvas setting
canvas_config = config.get("canvas") canvas_config = config.get("canvas")
logging.debug("canvas metadata: %s", canvas_config) logging.debug("canvas metadata: %s", canvas_config)
@ -392,7 +437,7 @@ class CoreClient:
except ValueError: except ValueError:
logging.exception("unknown shape: %s", shape_type) logging.exception("unknown shape: %s", shape_type)
def create_new_session(self): def create_new_session(self) -> None:
""" """
Create a new session Create a new session
""" """
@ -400,7 +445,7 @@ class CoreClient:
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 = 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,
@ -413,7 +458,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("New Session Error", e) self.app.show_grpc_exception("New Session Error", e)
def delete_session(self, session_id: int = None): def delete_session(self, session_id: int = None) -> None:
if session_id is None: if session_id is None:
session_id = self.session_id session_id = self.session_id
try: try:
@ -422,7 +467,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Delete Session Error", e) self.app.show_grpc_exception("Delete Session Error", e)
def setup(self): def setup(self) -> None:
""" """
Query sessions, if there exist any, prompt whether to join one Query sessions, if there exist any, prompt whether to join one
""" """
@ -457,7 +502,7 @@ class CoreClient:
dialog.show() dialog.show()
self.app.close() self.app.close()
def edit_node(self, core_node: core_pb2.Node): def edit_node(self, core_node: Node) -> None:
try: try:
self.client.edit_node( self.client.edit_node(
self.session_id, core_node.id, core_node.position, source=GUI_SOURCE self.session_id, core_node.id, core_node.position, source=GUI_SOURCE
@ -465,17 +510,17 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Edit Node Error", e) self.app.show_grpc_exception("Edit Node Error", e)
def start_session(self) -> core_pb2.StartSessionResponse: def start_session(self) -> StartSessionResponse:
self.interfaces_manager.reset_mac() self.ifaces_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 = [] links = []
for edge in self.links.values(): for edge in self.links.values():
link = core_pb2.Link() link = Link()
link.CopyFrom(edge.link) link.CopyFrom(edge.link)
if link.HasField("interface_one") and not link.interface_one.mac: if link.HasField("iface1") and not link.iface1.mac:
link.interface_one.mac = self.interfaces_manager.next_mac() link.iface1.mac = self.ifaces_manager.next_mac()
if link.HasField("interface_two") and not link.interface_two.mac: if link.HasField("iface2") and not link.iface2.mac:
link.interface_two.mac = self.interfaces_manager.next_mac() link.iface2.mac = self.ifaces_manager.next_mac()
links.append(link) 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()
@ -491,7 +536,7 @@ class CoreClient:
emane_config = {x: self.emane_config[x].value for x in self.emane_config} emane_config = {x: self.emane_config[x].value for x in self.emane_config}
else: else:
emane_config = None emane_config = None
response = core_pb2.StartSessionResponse(result=False) response = StartSessionResponse(result=False)
try: try:
response = self.client.start_session( response = self.client.start_session(
self.session_id, self.session_id,
@ -517,10 +562,10 @@ class CoreClient:
self.app.show_grpc_exception("Start Session Error", e) 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) -> StopSessionResponse:
if not session_id: if not session_id:
session_id = self.session_id session_id = self.session_id
response = core_pb2.StopSessionResponse(result=False) response = StopSessionResponse(result=False)
try: try:
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)
@ -528,9 +573,9 @@ class CoreClient:
self.app.show_grpc_exception("Stop Session Error", e) self.app.show_grpc_exception("Stop Session Error", e)
return response return response
def show_mobility_players(self): def show_mobility_players(self) -> None:
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if canvas_node.mobility_config: if canvas_node.mobility_config:
mobility_player = MobilityPlayer( mobility_player = MobilityPlayer(
@ -540,7 +585,7 @@ class CoreClient:
self.mobility_players[node_id] = mobility_player self.mobility_players[node_id] = mobility_player
mobility_player.show() mobility_player.show()
def set_metadata(self): def set_metadata(self) -> None:
# create canvas data # create canvas data
wallpaper = None wallpaper = None
if self.app.canvas.wallpaper_file: if self.app.canvas.wallpaper_file:
@ -564,7 +609,7 @@ class CoreClient:
response = self.client.set_session_metadata(self.session_id, metadata) response = self.client.set_session_metadata(self.session_id, metadata)
logging.info("set session metadata %s, result: %s", metadata, response) logging.info("set session metadata %s, result: %s", metadata, response)
def launch_terminal(self, node_id: int): def launch_terminal(self, node_id: int) -> None:
try: try:
terminal = self.app.guiconfig.preferences.terminal terminal = self.app.guiconfig.preferences.terminal
if not terminal: if not terminal:
@ -581,12 +626,12 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Node Terminal Error", e) self.app.show_grpc_exception("Node Terminal Error", e)
def save_xml(self, file_path: str): def save_xml(self, file_path: str) -> None:
""" """
Save core session as to an xml file Save core session as to an xml file
""" """
try: try:
if self.state != core_pb2.SessionState.RUNTIME: if self.state != SessionState.RUNTIME:
logging.debug("Send session data to the daemon") logging.debug("Send session data to the daemon")
self.send_data() self.send_data()
response = self.client.save_xml(self.session_id, file_path) response = self.client.save_xml(self.session_id, file_path)
@ -594,7 +639,7 @@ class CoreClient:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Save XML Error", e) self.app.show_grpc_exception("Save XML Error", e)
def open_xml(self, file_path: str): def open_xml(self, file_path: str) -> None:
""" """
Open core xml Open core xml
""" """
@ -633,7 +678,8 @@ class CoreClient:
shutdown=shutdowns, shutdown=shutdowns,
) )
logging.info( logging.info(
"Set %s service for node(%s), files: %s, Startup: %s, Validation: %s, Shutdown: %s, Result: %s", "Set %s service for node(%s), files: %s, Startup: %s, "
"Validation: %s, Shutdown: %s, Result: %s",
service_name, service_name,
node_id, node_id,
files, files,
@ -662,7 +708,7 @@ class CoreClient:
def set_node_service_file( def set_node_service_file(
self, node_id: int, service_name: str, file_name: str, data: str self, node_id: int, service_name: str, file_name: str, data: str
): ) -> None:
response = self.client.set_node_service_file( response = self.client.set_node_service_file(
self.session_id, node_id, service_name, file_name, data self.session_id, node_id, service_name, file_name, data
) )
@ -675,35 +721,33 @@ class CoreClient:
response, response,
) )
def create_nodes_and_links(self): def create_nodes_and_links(self) -> None:
""" """
create nodes and links that have not been created yet create nodes and links that have not been created yet
""" """
node_protos = [x.core_node for x in self.canvas_nodes.values()] node_protos = [x.core_node for x in self.canvas_nodes.values()]
link_protos = [x.link for x in self.links.values()] link_protos = [x.link for x in self.links.values()]
if self.state != core_pb2.SessionState.DEFINITION: if self.state != SessionState.DEFINITION:
self.client.set_session_state( self.client.set_session_state(self.session_id, SessionState.DEFINITION)
self.session_id, core_pb2.SessionState.DEFINITION
)
self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) self.client.set_session_state(self.session_id, SessionState.DEFINITION)
for node_proto in node_protos: for node_proto in node_protos:
response = self.client.add_node(self.session_id, node_proto) response = self.client.add_node(self.session_id, node_proto)
logging.debug("create node: %s", response) logging.debug("create node: %s", response)
for link_proto in link_protos: for link_proto in link_protos:
response = self.client.add_link( response = self.client.add_link(
self.session_id, self.session_id,
link_proto.node_one_id, link_proto.node1_id,
link_proto.node_two_id, link_proto.node2_id,
link_proto.interface_one, link_proto.iface1,
link_proto.interface_two, link_proto.iface2,
link_proto.options, link_proto.options,
) )
logging.debug("create link: %s", response) logging.debug("create link: %s", response)
def send_data(self): def send_data(self) -> None:
""" """
send to daemon all session info, but don't start the session Send to daemon all session info, but don't start the session
""" """
self.create_nodes_and_links() self.create_nodes_and_links()
for config_proto in self.get_wlan_configs_proto(): for config_proto in self.get_wlan_configs_proto():
@ -739,15 +783,25 @@ class CoreClient:
config_proto.node_id, config_proto.node_id,
config_proto.model, config_proto.model,
config_proto.config, config_proto.config,
config_proto.interface_id, config_proto.iface_id,
) )
if self.emane_config: if self.emane_config:
config = {x: self.emane_config[x].value for x in self.emane_config} config = {x: self.emane_config[x].value for x in self.emane_config}
self.client.set_emane_config(self.session_id, config) self.client.set_emane_config(self.session_id, config)
if self.location:
self.client.set_session_location(
self.session_id,
self.location.x,
self.location.y,
self.location.z,
self.location.lat,
self.location.lon,
self.location.alt,
self.location.scale,
)
self.set_metadata() self.set_metadata()
def close(self): def close(self) -> None:
""" """
Clean ups when done using grpc Clean ups when done using grpc
""" """
@ -766,31 +820,31 @@ class CoreClient:
return i return i
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: NodeType, model: str
) -> Optional[core_pb2.Node]: ) -> Optional[Node]:
""" """
Add node, with information filled in, to grpc manager Add node, with information filled in, to grpc manager
""" """
node_id = self.next_node_id() node_id = self.next_node_id()
position = core_pb2.Position(x=x, y=y) position = Position(x=x, y=y)
image = None image = None
if NodeUtils.is_image_node(node_type): if NodeUtils.is_image_node(node_type):
image = "ubuntu:latest" image = "ubuntu:latest"
emane = None emane = None
if node_type == core_pb2.NodeType.EMANE: if node_type == NodeType.EMANE:
if not self.emane_models: if not self.emane_models:
dialog = EmaneInstallDialog(self.app) dialog = EmaneInstallDialog(self.app)
dialog.show() dialog.show()
return 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 == NodeType.WIRELESS_LAN:
name = f"WLAN{node_id}" name = f"WLAN{node_id}"
elif node_type in [core_pb2.NodeType.RJ45, core_pb2.NodeType.TUNNEL]: elif node_type in [NodeType.RJ45, NodeType.TUNNEL]:
name = "UNASSIGNED" name = "UNASSIGNED"
else: else:
name = f"n{node_id}" name = f"n{node_id}"
node = core_pb2.Node( node = Node(
id=node_id, id=node_id,
type=node_type, type=node_type,
name=name, name=name,
@ -816,7 +870,7 @@ class CoreClient:
) )
return node return node
def deleted_graph_nodes(self, canvas_nodes: List[core_pb2.Node]): def deleted_graph_nodes(self, canvas_nodes: List[Node]) -> None:
""" """
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
@ -830,35 +884,35 @@ class CoreClient:
for edge in edges: for edge in edges:
del self.links[edge.token] del self.links[edge.token]
links.append(edge.link) links.append(edge.link)
self.interfaces_manager.removed(links) self.ifaces_manager.removed(links)
def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface: def create_iface(self, canvas_node: CanvasNode) -> Interface:
node = canvas_node.core_node node = canvas_node.core_node
ip4, ip6 = self.interfaces_manager.get_ips(node) ip4, ip6 = self.ifaces_manager.get_ips(node)
ip4_mask = self.interfaces_manager.ip4_mask ip4_mask = self.ifaces_manager.ip4_mask
ip6_mask = self.interfaces_manager.ip6_mask ip6_mask = self.ifaces_manager.ip6_mask
interface_id = canvas_node.next_interface_id() iface_id = canvas_node.next_iface_id()
name = f"eth{interface_id}" name = f"eth{iface_id}"
interface = core_pb2.Interface( iface = Interface(
id=interface_id, id=iface_id,
name=name, name=name,
ip4=ip4, ip4=ip4,
ip4mask=ip4_mask, ip4_mask=ip4_mask,
ip6=ip6, ip6=ip6,
ip6mask=ip6_mask, ip6_mask=ip6_mask,
) )
logging.info( logging.info(
"create node(%s) interface(%s) IPv4(%s) IPv6(%s)", "create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
node.name, node.name,
interface.name, iface.name,
interface.ip4, iface.ip4,
interface.ip6, iface.ip6,
) )
return interface return iface
def create_link( def create_link(
self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
): ) -> None:
""" """
Create core link for a pair of canvas nodes, with token referencing Create core link for a pair of canvas nodes, with token referencing
the canvas edge. the canvas edge.
@ -867,34 +921,34 @@ class CoreClient:
dst_node = canvas_dst_node.core_node dst_node = canvas_dst_node.core_node
# determine subnet # determine subnet
self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node) self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
src_interface = None src_iface = 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_iface = self.create_iface(canvas_src_node)
self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token
dst_interface = None dst_iface = 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_iface = self.create_iface(canvas_dst_node)
self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
link = core_pb2.Link( link = Link(
type=core_pb2.LinkType.WIRED, type=LinkType.WIRED,
node_one_id=src_node.id, node1_id=src_node.id,
node_two_id=dst_node.id, node2_id=dst_node.id,
interface_one=src_interface, iface1=src_iface,
interface_two=dst_interface, iface2=dst_iface,
) )
# assign after creating link proto, since interfaces are copied # assign after creating link proto, since interfaces are copied
if src_interface: if src_iface:
interface_one = link.interface_one iface1 = link.iface1
edge.src_interface = interface_one edge.src_iface = iface1
canvas_src_node.interfaces[interface_one.id] = interface_one canvas_src_node.ifaces[iface1.id] = iface1
if dst_interface: if dst_iface:
interface_two = link.interface_two iface2 = link.iface2
edge.dst_interface = interface_two edge.dst_iface = iface2
canvas_dst_node.interfaces[interface_two.id] = interface_two canvas_dst_node.ifaces[iface2.id] = iface2
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)
@ -902,7 +956,7 @@ class CoreClient:
def get_wlan_configs_proto(self) -> List[WlanConfig]: def get_wlan_configs_proto(self) -> List[WlanConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if not canvas_node.wlan_config: if not canvas_node.wlan_config:
continue continue
@ -916,7 +970,7 @@ class CoreClient:
def get_mobility_configs_proto(self) -> List[MobilityConfig]: def get_mobility_configs_proto(self) -> List[MobilityConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: if canvas_node.core_node.type != NodeType.WIRELESS_LAN:
continue continue
if not canvas_node.mobility_config: if not canvas_node.mobility_config:
continue continue
@ -930,16 +984,16 @@ class CoreClient:
def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]: def get_emane_model_configs_proto(self) -> List[EmaneModelConfig]:
configs = [] configs = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if canvas_node.core_node.type != core_pb2.NodeType.EMANE: if canvas_node.core_node.type != NodeType.EMANE:
continue continue
node_id = canvas_node.core_node.id node_id = canvas_node.core_node.id
for key, config in canvas_node.emane_model_configs.items(): for key, config in canvas_node.emane_model_configs.items():
model, interface = key model, iface_id = key
config = {x: config[x].value for x in config} config = {x: config[x].value for x in config}
if interface is None: if iface_id is None:
interface = -1 iface_id = -1
config_proto = EmaneModelConfig( config_proto = EmaneModelConfig(
node_id=node_id, interface_id=interface, model=model, config=config node_id=node_id, iface_id=iface_id, model=model, config=config
) )
configs.append(config_proto) configs.append(config_proto)
return configs return configs
@ -981,9 +1035,7 @@ class CoreClient:
configs.append(config_proto) configs.append(config_proto)
return configs return configs
def get_config_service_configs_proto( def get_config_service_configs_proto(self) -> List[ConfigServiceConfig]:
self
) -> List[configservices_pb2.ConfigServiceConfig]:
config_service_protos = [] config_service_protos = []
for canvas_node in self.canvas_nodes.values(): for canvas_node in self.canvas_nodes.values():
if not NodeUtils.is_container_node(canvas_node.core_node.type): if not NodeUtils.is_container_node(canvas_node.core_node.type):
@ -993,7 +1045,7 @@ class CoreClient:
node_id = canvas_node.core_node.id node_id = canvas_node.core_node.id
for name, service_config in canvas_node.config_service_configs.items(): 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 = ConfigServiceConfig(
node_id=node_id, node_id=node_id,
name=name, name=name,
templates=service_config["templates"], templates=service_config["templates"],
@ -1006,7 +1058,7 @@ class CoreClient:
logging.info("running node(%s) cmd: %s", node_id, self.observer) logging.info("running node(%s) cmd: %s", node_id, self.observer)
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, ConfigOption]:
response = self.client.get_wlan_config(self.session_id, node_id) response = self.client.get_wlan_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug( logging.debug(
@ -1016,7 +1068,7 @@ class CoreClient:
) )
return dict(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, ConfigOption]:
response = self.client.get_mobility_config(self.session_id, node_id) response = self.client.get_mobility_config(self.session_id, node_id)
config = response.config config = response.config
logging.debug( logging.debug(
@ -1027,24 +1079,25 @@ class CoreClient:
return dict(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, iface_id: int = None
) -> Dict[str, common_pb2.ConfigOption]: ) -> Dict[str, ConfigOption]:
if interface is None: if iface_id is None:
interface = -1 iface_id = -1
response = self.client.get_emane_model_config( response = self.client.get_emane_model_config(
self.session_id, node_id, model, interface self.session_id, node_id, model, iface_id
) )
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,
model, model,
interface, iface_id,
config, config,
) )
return dict(config) return dict(config)
def execute_script(self, script): def execute_script(self, script) -> None:
response = self.client.execute_script(script) response = self.client.execute_script(script)
logging.info("execute python script %s", response) logging.info("execute python script %s", response)
if response.session_id != -1: if response.session_id != -1:

View file

@ -35,11 +35,11 @@ THE POSSIBILITY OF SUCH DAMAGE.\
class AboutDialog(Dialog): class AboutDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "About CORE") super().__init__(app, "About CORE")
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)

View file

@ -3,9 +3,9 @@ check engine light
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
from core.api.grpc.core_pb2 import ExceptionLevel from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
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 CodeText from core.gui.widgets import CodeText
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog): class AlertsDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Alerts") super().__init__(app, "Alerts")
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.codetext = None self.codetext: Optional[CodeText] = None
self.alarm_map = {} self.alarm_map: Dict[int, ExceptionEvent] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.top.rowconfigure(1, weight=1) self.top.rowconfigure(1, weight=1)
@ -97,13 +97,13 @@ class AlertsDialog(Dialog):
button = ttk.Button(frame, text="Close", command=self.destroy) button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def reset_alerts(self): def reset_alerts(self) -> None:
self.codetext.text.delete("1.0", tk.END) self.codetext.text.delete("1.0", tk.END)
for item in self.tree.get_children(): for item in self.tree.get_children():
self.tree.delete(item) self.tree.delete(item)
self.app.statusbar.core_alarms.clear() self.app.statusbar.core_alarms.clear()
def click_select(self, event: tk.Event): def click_select(self, event: tk.Event) -> None:
current = self.tree.selection()[0] current = self.tree.selection()[0]
alarm = self.alarm_map[current] alarm = self.alarm_map[current]
self.codetext.text.config(state=tk.NORMAL) self.codetext.text.config(state=tk.NORMAL)

View file

@ -7,38 +7,43 @@ from typing import TYPE_CHECKING
from core.gui import validation from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
PIXEL_SCALE = 100 PIXEL_SCALE: int = 100
class SizeAndScaleDialog(Dialog): class SizeAndScaleDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
""" """
create an instance for size and scale object create an instance for size and scale object
""" """
super().__init__(app, "Canvas Size and Scale") super().__init__(app, "Canvas Size and Scale")
self.canvas = self.app.canvas self.canvas: CanvasGraph = self.app.canvas
self.section_font = font.Font(weight="bold") self.section_font: font.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 = tk.IntVar(value=width)
self.pixel_height = tk.IntVar(value=height) self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.location location = self.app.core.location
self.x = tk.DoubleVar(value=location.x) self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
self.y = tk.DoubleVar(value=location.y) self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
self.lat = tk.DoubleVar(value=location.lat) self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
self.lon = tk.DoubleVar(value=location.lon) self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon)
self.alt = tk.DoubleVar(value=location.alt) self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt)
self.scale = tk.DoubleVar(value=location.scale) self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale)
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_width: tk.IntVar = tk.IntVar(
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) value=width / PIXEL_SCALE * location.scale
self.save_default = tk.BooleanVar(value=False) )
self.meters_height: tk.IntVar = tk.IntVar(
value=height / PIXEL_SCALE * location.scale
)
self.save_default: tk.BooleanVar = tk.BooleanVar(value=False)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_size() self.draw_size()
self.draw_scale() self.draw_scale()
@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_size(self): def draw_size(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -61,10 +66,12 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
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 = validation.PositiveIntEntry(frame, textvariable=self.pixel_height) entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX) entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
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")
@ -75,16 +82,20 @@ 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 = validation.PositiveFloatEntry(frame, textvariable=self.meters_width) entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_width, state=tk.DISABLED
)
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 = validation.PositiveFloatEntry(frame, textvariable=self.meters_height) entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_height, state=tk.DISABLED
)
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")
def draw_scale(self): def draw_scale(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -96,10 +107,11 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX) label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale) entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX) entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
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")
def draw_reference_point(self): def draw_reference_point(self) -> None:
label_frame = ttk.Labelframe( label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD self.top, text="Reference Point", padding=FRAME_PAD
) )
@ -150,13 +162,13 @@ class SizeAndScaleDialog(Dialog):
entry = validation.FloatEntry(frame, textvariable=self.alt) entry = validation.FloatEntry(frame, textvariable=self.alt)
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) -> None:
button = ttk.Checkbutton( button = ttk.Checkbutton(
self.top, text="Save as default?", variable=self.save_default self.top, text="Save as default?", variable=self.save_default
) )
button.grid(sticky="w", pady=PADY) button.grid(sticky="w", pady=PADY)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -168,7 +180,14 @@ class SizeAndScaleDialog(Dialog):
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_apply(self): def size_scale_keyup(self, _event: tk.Event) -> None:
scale = self.scale.get()
width = self.pixel_width.get()
height = self.pixel_height.get()
self.meters_width.set(width / PIXEL_SCALE * scale)
self.meters_height.set(height / PIXEL_SCALE * scale)
def click_apply(self) -> None:
width, height = self.pixel_width.get(), self.pixel_height.get() width, height = self.pixel_width.get(), self.pixel_height.get()
self.canvas.redraw_canvas((width, height)) self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper: if self.canvas.wallpaper:

View file

@ -4,10 +4,11 @@ set wallpaper
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 from typing import TYPE_CHECKING, List, Optional
from core.gui.appconfig import BACKGROUNDS_PATH from core.gui.appconfig import BACKGROUNDS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.images import Images from core.gui.images import Images
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import image_chooser from core.gui.widgets import image_chooser
@ -17,20 +18,22 @@ if TYPE_CHECKING:
class CanvasWallpaperDialog(Dialog): class CanvasWallpaperDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
""" """
create an instance of CanvasWallpaper object create an instance of CanvasWallpaper object
""" """
super().__init__(app, "Canvas Background") super().__init__(app, "Canvas Background")
self.canvas = self.app.canvas self.canvas: CanvasGraph = self.app.canvas
self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) self.scale_option: tk.IntVar = tk.IntVar(value=self.canvas.scale_option.get())
self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(
self.filename = tk.StringVar(value=self.canvas.wallpaper_file) value=self.canvas.adjust_to_dim.get()
self.image_label = None )
self.options = [] self.filename: tk.StringVar = tk.StringVar(value=self.canvas.wallpaper_file)
self.image_label: Optional[ttk.Label] = None
self.options: List[ttk.Radiobutton] = []
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_image() self.draw_image()
self.draw_image_label() self.draw_image_label()
@ -40,19 +43,19 @@ class CanvasWallpaperDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_image(self): def draw_image(self) -> None:
self.image_label = ttk.Label( self.image_label = ttk.Label(
self.top, text="(image preview)", width=32, anchor=tk.CENTER self.top, text="(image preview)", width=32, anchor=tk.CENTER
) )
self.image_label.grid(pady=PADY) self.image_label.grid(pady=PADY)
def draw_image_label(self): def draw_image_label(self) -> None:
label = ttk.Label(self.top, text="Image filename: ") label = ttk.Label(self.top, text="Image filename: ")
label.grid(sticky="ew") label.grid(sticky="ew")
if self.filename.get(): if self.filename.get():
self.draw_preview() self.draw_preview()
def draw_image_selection(self): def draw_image_selection(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=2) frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -69,7 +72,7 @@ class CanvasWallpaperDialog(Dialog):
button = ttk.Button(frame, text="Clear", command=self.click_clear) button = ttk.Button(frame, text="Clear", command=self.click_clear)
button.grid(row=0, column=2, sticky="ew") button.grid(row=0, column=2, sticky="ew")
def draw_options(self): def draw_options(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -101,7 +104,7 @@ class CanvasWallpaperDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
self.options.append(button) self.options.append(button)
def draw_additional_options(self): def draw_additional_options(self) -> None:
checkbutton = ttk.Checkbutton( checkbutton = ttk.Checkbutton(
self.top, self.top,
text="Adjust canvas size to image dimensions", text="Adjust canvas size to image dimensions",
@ -110,7 +113,7 @@ class CanvasWallpaperDialog(Dialog):
) )
checkbutton.grid(sticky="ew", padx=PADX) checkbutton.grid(sticky="ew", padx=PADX)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -122,18 +125,18 @@ class CanvasWallpaperDialog(Dialog):
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_open_image(self): def click_open_image(self) -> None:
filename = image_chooser(self, BACKGROUNDS_PATH) filename = image_chooser(self, BACKGROUNDS_PATH)
if filename: if filename:
self.filename.set(filename) self.filename.set(filename)
self.draw_preview() self.draw_preview()
def draw_preview(self): def draw_preview(self) -> None:
image = Images.create(self.filename.get(), 250, 135) image = Images.create(self.filename.get(), 250, 135)
self.image_label.config(image=image) self.image_label.config(image=image)
self.image_label.image = image self.image_label.image = image
def click_clear(self): def click_clear(self) -> None:
""" """
delete like shown in image link entry if there is any delete like shown in image link entry if there is any
""" """
@ -143,7 +146,7 @@ class CanvasWallpaperDialog(Dialog):
self.image_label.config(image="", width=32) self.image_label.config(image="", width=32)
self.image_label.image = None self.image_label.image = None
def click_adjust_canvas(self): def click_adjust_canvas(self) -> None:
# deselect all radio buttons and grey them out # deselect all radio buttons and grey them out
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
self.scale_option.set(0) self.scale_option.set(0)
@ -155,7 +158,7 @@ class CanvasWallpaperDialog(Dialog):
for option in self.options: for option in self.options:
option.config(state=tk.NORMAL) option.config(state=tk.NORMAL)
def click_apply(self): def click_apply(self) -> None:
self.canvas.scale_option.set(self.scale_option.get()) self.canvas.scale_option.set(self.scale_option.get())
self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get())
self.canvas.show_grid.click_handler() self.canvas.show_grid.click_handler()

View file

@ -3,7 +3,7 @@ custom color picker
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Tuple
from core.gui import validation from core.gui import validation
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -18,23 +18,23 @@ class ColorPickerDialog(Dialog):
self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000" self, master: tk.BaseWidget, app: "Application", initcolor: str = "#000000"
): ):
super().__init__(app, "Color Picker", master=master) super().__init__(app, "Color Picker", master=master)
self.red_entry = None self.red_entry: Optional[validation.RgbEntry] = None
self.blue_entry = None self.blue_entry: Optional[validation.RgbEntry] = None
self.green_entry = None self.green_entry: Optional[validation.RgbEntry] = None
self.hex_entry = None self.hex_entry: Optional[validation.HexEntry] = None
self.red_label = None self.red_label: Optional[ttk.Label] = None
self.green_label = None self.green_label: Optional[ttk.Label] = None
self.blue_label = None self.blue_label: Optional[ttk.Label] = None
self.display = None self.display: Optional[tk.Frame] = None
self.color = initcolor self.color: str = initcolor
red, green, blue = self.get_rgb(initcolor) red, green, blue = self.get_rgb(initcolor)
self.red = tk.IntVar(value=red) self.red: tk.IntVar = tk.IntVar(value=red)
self.blue = tk.IntVar(value=blue) self.blue: tk.IntVar = tk.IntVar(value=blue)
self.green = tk.IntVar(value=green) self.green: tk.IntVar = tk.IntVar(value=green)
self.hex = tk.StringVar(value=initcolor) self.hex: tk.StringVar = tk.StringVar(value=initcolor)
self.red_scale = tk.IntVar(value=red) self.red_scale: tk.IntVar = tk.IntVar(value=red)
self.green_scale = tk.IntVar(value=green) self.green_scale: tk.IntVar = tk.IntVar(value=green)
self.blue_scale = tk.IntVar(value=blue) self.blue_scale: tk.IntVar = tk.IntVar(value=blue)
self.draw() self.draw()
self.set_bindings() self.set_bindings()
@ -42,7 +42,7 @@ class ColorPickerDialog(Dialog):
self.show() self.show()
return self.color return self.color
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(3, weight=1) self.top.rowconfigure(3, weight=1)
@ -136,7 +136,7 @@ class ColorPickerDialog(Dialog):
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 set_bindings(self): def set_bindings(self) -> None:
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.green_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb")) self.blue_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
@ -146,7 +146,7 @@ class ColorPickerDialog(Dialog):
self.blue.trace_add("write", self.update_color) self.blue.trace_add("write", self.update_color)
self.hex.trace_add("write", self.update_color) self.hex.trace_add("write", self.update_color)
def button_ok(self): def button_ok(self) -> None:
self.color = self.hex.get() self.color = self.hex.get()
self.destroy() self.destroy()
@ -159,10 +159,10 @@ class ColorPickerDialog(Dialog):
green = self.green_entry.get() green = self.green_entry.get()
return "#%02x%02x%02x" % (int(red), int(green), int(blue)) return "#%02x%02x%02x" % (int(red), int(green), int(blue))
def current_focus(self, focus: str): def current_focus(self, focus: str) -> None:
self.focus = focus self.focus = focus
def update_color(self, arg1=None, arg2=None, arg3=None): def update_color(self, arg1=None, arg2=None, arg3=None) -> None:
if self.focus == "rgb": if self.focus == "rgb":
red = self.red_entry.get() red = self.red_entry.get()
blue = self.blue_entry.get() blue = self.blue_entry.get()
@ -184,7 +184,7 @@ class ColorPickerDialog(Dialog):
self.display.config(background=hex_code) self.display.config(background=hex_code)
self.set_label(str(red), str(green), str(blue)) self.set_label(str(red), str(green), str(blue))
def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar): def scale_callback(self, var: tk.IntVar, color_var: tk.IntVar) -> None:
color_var.set(var.get()) color_var.set(var.get())
self.focus = "rgb" self.focus = "rgb"
self.update_color() self.update_color()
@ -194,17 +194,17 @@ class ColorPickerDialog(Dialog):
self.green_scale.set(green) self.green_scale.set(green)
self.blue_scale.set(blue) self.blue_scale.set(blue)
def set_entry(self, red: int, green: int, blue: int): def set_entry(self, red: int, green: int, blue: int) -> None:
self.red.set(red) self.red.set(red)
self.green.set(green) self.green.set(green)
self.blue.set(blue) self.blue.set(blue)
def set_label(self, red: str, green: str, blue: str): def set_label(self, red: str, green: str, blue: str) -> None:
self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0))
self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0))
self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue)))
def get_rgb(self, hex_code: str) -> [int, int, int]: def get_rgb(self, hex_code: str) -> Tuple[int, int, int]:
""" """
convert a valid hex code to RGB values convert a valid hex code to RGB values
""" """

View file

@ -4,10 +4,11 @@ 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, List from typing import TYPE_CHECKING, Dict, List, Optional, Set
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
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.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
@ -16,6 +17,7 @@ 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 from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
class ConfigServiceConfigDialog(Dialog): class ConfigServiceConfigDialog(Dialog):
@ -26,56 +28,53 @@ class ConfigServiceConfigDialog(Dialog):
service_name: str, service_name: str,
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
node_id: int, node_id: int,
): ) -> None:
title = f"{service_name} Config Service" title = f"{service_name} Config Service"
super().__init__(app, title, master=master) super().__init__(app, title, master=master)
self.core = app.core self.core: "CoreClient" = app.core
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = node_id self.node_id: int = node_id
self.service_name = service_name self.service_name: str = service_name
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(2) self.radiovar.set(2)
self.directories = [] self.directories: List[str] = []
self.templates = [] self.templates: List[str] = []
self.dependencies = [] self.dependencies: List[str] = []
self.executables = [] self.executables: List[str] = []
self.startup_commands = [] self.startup_commands: List[str] = []
self.validation_commands = [] self.validation_commands: List[str] = []
self.shutdown_commands = [] self.shutdown_commands: List[str] = []
self.default_startup = [] self.default_startup: List[str] = []
self.default_validate = [] self.default_validate: List[str] = []
self.default_shutdown = [] self.default_shutdown: List[str] = []
self.validation_mode = None self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time = None self.validation_time: Optional[int] = None
self.validation_period = tk.StringVar() self.validation_period: tk.StringVar = tk.StringVar()
self.modes = [] self.modes: List[str] = []
self.mode_configs = {} self.mode_configs: Dict[str, str] = {}
self.notebook = None
self.templates_combobox = None
self.modes_combobox = None
self.startup_commands_listbox = None
self.shutdown_commands_listbox = None
self.validate_commands_listbox = None
self.validation_time_entry = None
self.validation_mode_entry = None
self.template_text = None
self.validation_period_entry = None
self.original_service_files = {}
self.temp_service_files = {}
self.modified_files = set()
self.config_frame = None
self.default_config = None
self.config = None
self.has_error = False
self.notebook: Optional[ttk.Notebook] = None
self.templates_combobox: Optional[ttk.Combobox] = None
self.modes_combobox: Optional[ttk.Combobox] = None
self.startup_commands_listbox: Optional[tk.Listbox] = None
self.shutdown_commands_listbox: Optional[tk.Listbox] = None
self.validate_commands_listbox: Optional[tk.Listbox] = None
self.validation_time_entry: Optional[ttk.Entry] = None
self.validation_mode_entry: Optional[ttk.Entry] = None
self.template_text: Optional[CodeText] = None
self.validation_period_entry: Optional[ttk.Entry] = None
self.original_service_files: Dict[str, str] = {}
self.temp_service_files: Dict[str, str] = {}
self.modified_files: Set[str] = set()
self.config_frame: Optional[ConfigFrame] = None
self.default_config: Dict[str, str] = {}
self.config: Dict[str, ConfigOption] = {}
self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def load(self): def load(self) -> None:
try: try:
self.core.create_nodes_and_links() self.core.create_nodes_and_links()
service = self.core.config_services[self.service_name] service = self.core.config_services[self.service_name]
@ -116,7 +115,7 @@ class ConfigServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Get Config Service Error", e) self.app.show_grpc_exception("Get Config Service Error", e)
self.has_error = True self.has_error = True
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -130,7 +129,7 @@ class ConfigServiceConfigDialog(Dialog):
self.draw_tab_validation() self.draw_tab_validation()
self.draw_buttons() self.draw_buttons()
def draw_tab_files(self): def draw_tab_files(self) -> None:
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)
@ -174,7 +173,7 @@ class ConfigServiceConfigDialog(Dialog):
) )
self.template_text.text.bind("<FocusOut>", self.update_template_file_data) self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
def draw_tab_config(self): def draw_tab_config(self) -> None:
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)
@ -198,7 +197,7 @@ class ConfigServiceConfigDialog(Dialog):
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
def draw_tab_startstop(self): def draw_tab_startstop(self) -> None:
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)
@ -239,7 +238,7 @@ class ConfigServiceConfigDialog(Dialog):
elif i == 2: elif i == 2:
self.validate_commands_listbox = listbox_scroll.listbox self.validate_commands_listbox = listbox_scroll.listbox
def draw_tab_validation(self): def draw_tab_validation(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD) tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="ew") tab.grid(sticky="ew")
tab.columnconfigure(0, weight=1) tab.columnconfigure(0, weight=1)
@ -298,7 +297,7 @@ class ConfigServiceConfigDialog(Dialog):
for dependency in self.dependencies: for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency) listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(4): for i in range(4):
@ -312,7 +311,7 @@ class ConfigServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) 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")
def click_apply(self): def click_apply(self) -> None:
current_listbox = self.master.current.listbox current_listbox = self.master.current.listbox
if not self.is_custom(): if not self.is_custom():
self.canvas_node.config_service_configs.pop(self.service_name, None) self.canvas_node.config_service_configs.pop(self.service_name, None)
@ -333,18 +332,18 @@ class ConfigServiceConfigDialog(Dialog):
current_listbox.itemconfig(all_current.index(self.service_name), bg="green") current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
self.destroy() self.destroy()
def handle_template_changed(self, event: tk.Event): def handle_template_changed(self, event: tk.Event) -> None:
template = self.templates_combobox.get() template = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end") self.template_text.text.delete(1.0, "end")
self.template_text.text.insert("end", self.temp_service_files[template]) self.template_text.text.insert("end", self.temp_service_files[template])
def handle_mode_changed(self, event: tk.Event): def handle_mode_changed(self, event: tk.Event) -> None:
mode = self.modes_combobox.get() mode = self.modes_combobox.get()
config = self.mode_configs[mode] config = self.mode_configs[mode]
logging.info("mode config: %s", config) logging.info("mode config: %s", config)
self.config_frame.set_values(config) self.config_frame.set_values(config)
def update_template_file_data(self, event: tk.Event): def update_template_file_data(self, event: tk.Event) -> None:
scrolledtext = event.widget scrolledtext = event.widget
template = self.templates_combobox.get() template = self.templates_combobox.get()
self.temp_service_files[template] = scrolledtext.get(1.0, "end") self.temp_service_files[template] = scrolledtext.get(1.0, "end")
@ -353,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
else: else:
self.modified_files.discard(template) self.modified_files.discard(template)
def is_custom(self): def is_custom(self) -> bool:
has_custom_templates = len(self.modified_files) > 0 has_custom_templates = len(self.modified_files) > 0
has_custom_config = False has_custom_config = False
if self.config_frame: if self.config_frame:
@ -361,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog):
has_custom_config = self.default_config != current has_custom_config = self.default_config != current
return has_custom_templates or has_custom_config return has_custom_templates or has_custom_config
def click_defaults(self): def click_defaults(self) -> None:
self.canvas_node.config_service_configs.pop(self.service_name, None) self.canvas_node.config_service_configs.pop(self.service_name, None)
logging.info( logging.info(
"cleared config service config: %s", self.canvas_node.config_service_configs "cleared config service config: %s", self.canvas_node.config_service_configs
@ -374,12 +373,12 @@ class ConfigServiceConfigDialog(Dialog):
logging.info("resetting defaults: %s", self.default_config) logging.info("resetting defaults: %s", self.default_config)
self.config_frame.set_values(self.default_config) self.config_frame.set_values(self.default_config)
def click_copy(self): def click_copy(self) -> None:
pass pass
def append_commands( def append_commands(
self, commands: List[str], listbox: tk.Listbox, to_add: List[str] self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
): ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)

View file

@ -4,81 +4,58 @@ 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, Tuple from typing import TYPE_CHECKING, Dict, Optional
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 PADX, PADY
from core.gui.widgets import CodeText 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.dialogs.serviceconfig import ServiceConfigDialog
class CopyServiceConfigDialog(Dialog): class CopyServiceConfigDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int): def __init__(
super().__init__(app, f"Copy services to node {node_id}", master=master) self,
self.parent = master app: "Application",
self.node_id = node_id dialog: "ServiceConfigDialog",
self.service_configs = app.core.service_configs name: str,
self.file_configs = app.core.file_configs service: str,
file_name: str,
self.tree = None ) -> None:
super().__init__(app, f"Copy Custom File to {name}", master=dialog)
self.dialog: "ServiceConfigDialog" = dialog
self.service: str = service
self.file_name: str = file_name
self.listbox: Optional[tk.Listbox] = None
self.nodes: Dict[str, int] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.tree = ttk.Treeview(self.top) self.top.rowconfigure(1, weight=1)
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX) label = ttk.Label(
self.tree["columns"] = () self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
self.tree.column("#0", width=270, minwidth=270, stretch=tk.YES)
self.tree.heading("#0", text="Service configuration items", anchor=tk.CENTER)
custom_nodes = set(self.service_configs).union(set(self.file_configs))
for nid in custom_nodes:
treeid = self.tree.insert("", "end", text=f"n{nid}", tags="node")
services = self.service_configs.get(nid, None)
files = self.file_configs.get(nid, None)
tree_ids = {}
if services:
for service, config in services.items():
serviceid = self.tree.insert(
treeid, "end", text=service, tags="service"
) )
tree_ids[service] = serviceid label.grid(sticky="ew", pady=PADY)
cmdup = config.startup[:]
cmddown = config.shutdown[:] listbox_scroll = ListboxScroll(self.top)
cmdval = config.validate[:] listbox_scroll.grid(sticky="nsew", pady=PADY)
self.tree.insert( self.listbox = listbox_scroll.listbox
serviceid, for canvas_node in self.app.canvas.nodes.values():
"end", file_configs = canvas_node.service_file_configs.get(self.service)
text=f"cmdup=({str(cmdup)[1:-1]})", if not file_configs:
tags=("cmd", "up"), continue
) data = file_configs.get(self.file_name)
self.tree.insert( if not data:
serviceid, continue
"end", name = canvas_node.core_node.name
text=f"cmddown=({str(cmddown)[1:-1]})", self.nodes[name] = canvas_node.id
tags=("cmd", "down"), self.listbox.insert(tk.END, name)
)
self.tree.insert(
serviceid,
"end",
text=f"cmdval=({str(cmdval)[1:-1]})",
tags=("cmd", "val"),
)
if files:
for service, configs in files.items():
if service in tree_ids:
serviceid = tree_ids[service]
else:
serviceid = self.tree.insert(
treeid, "end", text=service, tags="service"
)
tree_ids[service] = serviceid
for filename, data in configs.items():
self.tree.insert(serviceid, "end", text=filename, tags="file")
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(row=1, column=0) frame.grid(sticky="ew")
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Copy", command=self.click_copy) button = ttk.Button(frame, text="Copy", command=self.click_copy)
@ -86,118 +63,58 @@ class CopyServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="View", command=self.click_view) button = ttk.Button(frame, text="View", command=self.click_view)
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="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=2, sticky="ew", padx=PADX) button.grid(row=0, column=2, sticky="ew")
def click_copy(self): def click_copy(self) -> None:
selected = self.tree.selection() selection = self.listbox.curselection()
if selected: if not selection:
item = self.tree.item(selected[0]) return
if "file" in item["tags"]: name = self.listbox.get(selection)
filename = item["text"] canvas_node_id = self.nodes[name]
nid, service = self.get_node_service(selected) canvas_node = self.app.canvas.nodes[canvas_node_id]
data = self.file_configs[nid][service][filename] data = canvas_node.service_file_configs[self.service][self.file_name]
if service == self.parent.service_name: self.dialog.temp_service_files[self.file_name] = data
self.parent.temp_service_files[filename] = data self.dialog.modified_files.add(self.file_name)
self.parent.modified_files.add(filename) self.dialog.service_file_data.text.delete(1.0, tk.END)
if self.parent.filename_combobox.get() == filename: self.dialog.service_file_data.text.insert(tk.END, data)
self.parent.service_file_data.text.delete(1.0, "end")
self.parent.service_file_data.text.insert("end", data)
if "cmd" in item["tags"]:
nid, service = self.get_node_service(selected)
if service == self.master.service_name:
cmds = self.service_configs[nid][service]
if "up" in item["tags"]:
self.master.append_commands(
self.master.startup_commands,
self.master.startup_commands_listbox,
cmds.startup,
)
elif "down" in item["tags"]:
self.master.append_commands(
self.master.shutdown_commands,
self.master.shutdown_commands_listbox,
cmds.shutdown,
)
elif "val" in item["tags"]:
self.master.append_commands(
self.master.validate_commands,
self.master.validate_commands_listbox,
cmds.validate,
)
self.destroy() self.destroy()
def click_view(self): def click_view(self) -> None:
selected = self.tree.selection() selection = self.listbox.curselection()
data = "" if not selection:
if selected: return
item = self.tree.item(selected[0]) name = self.listbox.get(selection)
if "file" in item["tags"]: canvas_node_id = self.nodes[name]
nid, service = self.get_node_service(selected) canvas_node = self.app.canvas.nodes[canvas_node_id]
data = self.file_configs[nid][service][item["text"]] data = canvas_node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog( dialog = ViewConfigDialog(
self, self.app, nid, data, item["text"].split("/")[-1] self.app, self, name, self.service, self.file_name, data
) )
dialog.show() dialog.show()
if "cmd" in item["tags"]:
nid, service = self.get_node_service(selected)
cmds = self.service_configs[nid][service]
if "up" in item["tags"]:
data = f"({str(cmds.startup[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
elif "down" in item["tags"]:
data = f"({str(cmds.shutdown[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
elif "val" in item["tags"]:
data = f"({str(cmds.validate[:])[1:-1]})"
dialog = ViewConfigDialog(
self, self.app, self.node_id, data, "cmdup"
)
dialog.show()
def get_node_service(self, selected: Tuple[str]) -> [int, str]:
service_tree_id = self.tree.parent(selected[0])
service_name = self.tree.item(service_tree_id)["text"]
node_tree_id = self.tree.parent(service_tree_id)
node_id = int(self.tree.item(node_tree_id)["text"][1:])
return node_id, service_name
class ViewConfigDialog(Dialog): class ViewConfigDialog(Dialog):
def __init__( def __init__(
self, self,
master: tk.BaseWidget,
app: "Application", app: "Application",
node_id: int, master: tk.BaseWidget,
name: str,
service: str,
file_name: str,
data: str, data: str,
filename: str = None, ) -> None:
): title = f"{name} Service({service}) File({file_name})"
super().__init__(app, f"n{node_id} config data", master=master) super().__init__(app, title, 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.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
frame = ttk.Frame(self.top, padding=FRAME_PAD) self.top.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=10)
frame.grid(row=0, column=0, sticky="ew")
label = ttk.Label(frame, text="File: ")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
entry = ttk.Entry(frame, textvariable=self.filepath)
entry.config(state="disabled")
entry.grid(row=0, column=1, sticky="ew")
self.service_data = CodeText(self.top) self.service_data = CodeText(self.top)
self.service_data.grid(row=1, column=0, sticky="nsew") self.service_data.grid(sticky="nsew", pady=PADY)
self.service_data.text.insert("end", self.data) self.service_data.text.insert(tk.END, self.data)
self.service_data.text.config(state="disabled") self.service_data.text.config(state=tk.DISABLED)
button = ttk.Button(self.top, text="Close", command=self.destroy) button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(row=2, column=0, sticky="ew", padx=PADX) button.grid(sticky="ew")

View file

@ -2,7 +2,9 @@ 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, Set from typing import TYPE_CHECKING, Optional, Set
from PIL.ImageTk import PhotoImage
from core.gui import nodeutils from core.gui import nodeutils
from core.gui.appconfig import ICONS_PATH, CustomNode from core.gui.appconfig import ICONS_PATH, CustomNode
@ -19,15 +21,15 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog): class ServicesSelectDialog(Dialog):
def __init__( def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str] self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
): ) -> None:
super().__init__(app, "Node Services", master=master) super().__init__(app, "Node Services", master=master)
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
self.current_services = set(current_services) self.current_services: Set[str] = current_services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog):
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.groups.listbox.event_generate("<<ListboxSelect>>")
def handle_group_change(self, event: tk.Event): def handle_group_change(self, event: tk.Event) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.BooleanVar): def service_clicked(self, name: str, var: tk.BooleanVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
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:
@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog):
for name in sorted(self.current_services): for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
def click_cancel(self): def click_cancel(self) -> None:
self.current_services = None self.current_services = None
self.destroy() self.destroy()
class CustomNodesDialog(Dialog): class CustomNodesDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Custom Nodes") super().__init__(app, "Custom Nodes")
self.edit_button = None self.edit_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.nodes_list = None self.nodes_list: Optional[ListboxScroll] = None
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.image_button = None self.image_button: Optional[ttk.Button] = None
self.image = None self.image: Optional[PhotoImage] = None
self.image_file = None self.image_file: Optional[str] = None
self.services = set() self.services: Set[str] = set()
self.selected = None self.selected: Optional[str] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_node_config() self.draw_node_config()
self.draw_node_buttons() self.draw_node_buttons()
self.draw_buttons() self.draw_buttons()
def draw_node_config(self): def draw_node_config(self) -> None:
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog):
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")
def draw_node_buttons(self): def draw_node_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(3): for i in range(3):
@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog):
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 reset_values(self): def reset_values(self) -> None:
self.name.set("") self.name.set("")
self.image = None self.image = None
self.image_file = None self.image_file = None
self.services = set() self.services = set()
self.image_button.config(image="") self.image_button.config(image="")
def click_icon(self): def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH) file_path = image_chooser(self, ICONS_PATH)
if file_path: if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE) image = Images.create(file_path, nodeutils.ICON_SIZE)
@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog):
self.image_file = file_path self.image_file = file_path
self.image_button.config(image=self.image) self.image_button.config(image=self.image)
def click_services(self): def click_services(self) -> None:
dialog = ServicesSelectDialog(self, self.app, self.services) dialog = ServicesSelectDialog(self, self.app, self.services)
dialog.show() dialog.show()
if dialog.current_services is not None: if dialog.current_services is not None:
self.services.clear() self.services.clear()
self.services.update(dialog.current_services) self.services.update(dialog.current_services)
def click_save(self): def click_save(self) -> None:
self.app.guiconfig.nodes.clear() self.app.guiconfig.nodes.clear()
for name in 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]
custom_node = CustomNode(name, node_draw.image_file, node_draw.services) custom_node = CustomNode(
name, node_draw.image_file, list(node_draw.services)
)
self.app.guiconfig.nodes.append(custom_node) self.app.guiconfig.nodes.append(custom_node)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
name = self.name.get() name = self.name.get()
if name not in self.app.core.custom_nodes: if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem image_file = Path(self.image_file).stem
@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(tk.END, name) self.nodes_list.listbox.insert(tk.END, name)
self.reset_values() self.reset_values()
def click_edit(self): def click_edit(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -247,7 +251,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(self.selected_index, name) self.nodes_list.listbox.insert(self.selected_index, name)
self.nodes_list.listbox.selection_set(self.selected_index) self.nodes_list.listbox.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected and self.selected in self.app.core.custom_nodes: if self.selected and self.selected in self.app.core.custom_nodes:
self.nodes_list.listbox.delete(self.selected_index) self.nodes_list.listbox.delete(self.selected_index)
del self.app.core.custom_nodes[self.selected] del self.app.core.custom_nodes[self.selected]
@ -255,7 +259,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.selection_clear(0, tk.END) self.nodes_list.listbox.selection_clear(0, tk.END)
self.nodes_list.listbox.event_generate("<<ListboxSelect>>") self.nodes_list.listbox.event_generate("<<ListboxSelect>>")
def handle_node_select(self, event: tk.Event): def handle_node_select(self, event: tk.Event) -> None:
selection = self.nodes_list.listbox.curselection() selection = self.nodes_list.listbox.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -16,23 +16,23 @@ class Dialog(tk.Toplevel):
title: str, title: str,
modal: bool = True, modal: bool = True,
master: tk.BaseWidget = None, master: tk.BaseWidget = None,
): ) -> None:
if master is None: if master is None:
master = app master = app
super().__init__(master) super().__init__(master)
self.withdraw() self.withdraw()
self.app = app self.app: "Application" = app
self.modal = modal self.modal: bool = modal
self.title(title) self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy) self.protocol("WM_DELETE_WINDOW", self.destroy)
image = Images.get(ImageEnum.CORE, 16) image = Images.get(ImageEnum.CORE, 16)
self.tk.call("wm", "iconphoto", self._w, image) self.tk.call("wm", "iconphoto", self._w, image)
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.top = ttk.Frame(self, padding=DIALOG_PAD) self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
self.top.grid(sticky="nsew") self.top.grid(sticky="nsew")
def show(self): def show(self) -> None:
self.transient(self.master) self.transient(self.master)
self.focus_force() self.focus_force()
self.update() self.update()
@ -42,7 +42,7 @@ class Dialog(tk.Toplevel):
self.grab_set() self.grab_set()
self.wait_window() self.wait_window()
def draw_spacer(self, row: int = None): def draw_spacer(self, row: int = None) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(row=row, sticky="nsew") frame.grid(row=row, sticky="nsew")
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)

View file

@ -4,10 +4,12 @@ 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 from typing import TYPE_CHECKING, Dict, List, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
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 PADX, PADY from core.gui.themes import PADX, PADY
@ -19,32 +21,35 @@ if TYPE_CHECKING:
class GlobalEmaneDialog(Dialog): class GlobalEmaneDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(app, "EMANE Configuration", master=master) super().__init__(app, "EMANE Configuration", master=master)
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.enabled: bool = not self.app.core.is_runtime()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame = ConfigFrame(
self.top, self.app, self.app.core.emane_config, self.enabled
)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
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="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_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
self.destroy() self.destroy()
@ -56,71 +61,77 @@ class EmaneModelDialog(Dialog):
app: "Application", app: "Application",
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
model: str, model: str,
interface: int = None, iface_id: int = None,
): ) -> None:
super().__init__( super().__init__(
app, f"{canvas_node.core_node.name} {model} Configuration", master=master app, f"{canvas_node.core_node.name} {model} Configuration", master=master
) )
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.model = f"emane_{model}" self.model: str = f"emane_{model}"
self.interface = interface self.iface_id: int = iface_id
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.enabled: bool = not self.app.core.is_runtime()
self.has_error: bool = False
try: try:
self.config = self.canvas_node.emane_model_configs.get( config = self.canvas_node.emane_model_configs.get(
(self.model, self.interface) (self.model, self.iface_id)
) )
if not self.config: if not config:
self.config = self.app.core.get_emane_model_config( config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.interface self.node.id, self.model, self.iface_id
) )
self.config: Dict[str, ConfigOption] = config
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Get EMANE Config Error", e) self.app.show_grpc_exception("Get EMANE Config Error", e)
self.has_error = True self.has_error: bool = 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(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
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="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_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
key = (self.model, self.interface) key = (self.model, self.iface_id)
self.canvas_node.emane_model_configs[key] = self.config self.canvas_node.emane_model_configs[key] = self.config
self.destroy() self.destroy()
class EmaneConfigDialog(Dialog): class EmaneConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration") super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(1) self.radiovar.set(1)
self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] self.emane_models: List[str] = [
self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) x.split("_")[1] for x in self.app.core.emane_models
self.emane_model_button = None ]
model = self.node.emane.split("_")[1]
self.emane_model: tk.StringVar = tk.StringVar(value=model)
self.emane_model_button: Optional[ttk.Button] = None
self.enabled: bool = not self.app.core.is_runtime()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_emane_configuration() self.draw_emane_configuration()
self.draw_emane_models() self.draw_emane_models()
@ -128,14 +139,15 @@ class EmaneConfigDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_apply_and_cancel() self.draw_apply_and_cancel()
def draw_emane_configuration(self): def draw_emane_configuration(self) -> None:
""" """
draw the main frame for emane configuration draw the main frame for emane configuration
""" """
label = ttk.Label( label = ttk.Label(
self.top, self.top,
text="The EMANE emulation system provides more complex wireless radio emulation " text="The EMANE emulation system provides more complex wireless radio "
"\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", "emulation \nusing pluggable MAC and PHY modules. Refer to the wiki "
"for configuration option details",
justify=tk.CENTER, justify=tk.CENTER,
) )
label.grid(pady=PADY) label.grid(pady=PADY)
@ -153,7 +165,7 @@ class EmaneConfigDialog(Dialog):
button.image = image button.image = image
button.grid(sticky="ew", pady=PADY) button.grid(sticky="ew", pady=PADY)
def draw_emane_models(self): def draw_emane_models(self) -> None:
""" """
create a combobox that has all the known emane models create a combobox that has all the known emane models
""" """
@ -165,16 +177,14 @@ class EmaneConfigDialog(Dialog):
label.grid(row=0, column=0, sticky="w") label.grid(row=0, column=0, sticky="w")
# create combo box and its binding # create combo box and its binding
state = "readonly" if self.enabled else tk.DISABLED
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, frame, textvariable=self.emane_model, values=self.emane_models, state=state
textvariable=self.emane_model,
values=self.emane_models,
state="readonly",
) )
combobox.grid(row=0, column=1, sticky="ew") combobox.grid(row=0, column=1, sticky="ew")
combobox.bind("<<ComboboxSelected>>", self.emane_model_change) combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
def draw_emane_buttons(self): def draw_emane_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(2): for i in range(2):
@ -202,23 +212,22 @@ class EmaneConfigDialog(Dialog):
button.image = image button.image = image
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def draw_apply_and_cancel(self): def draw_apply_and_cancel(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply) button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
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): def click_emane_config(self) -> None:
dialog = GlobalEmaneDialog(self, self.app) dialog = GlobalEmaneDialog(self, self.app)
dialog.show() dialog.show()
def click_model_config(self): def click_model_config(self) -> None:
""" """
draw emane model configuration draw emane model configuration
""" """
@ -227,13 +236,13 @@ class EmaneConfigDialog(Dialog):
if not dialog.has_error: if not dialog.has_error:
dialog.show() dialog.show()
def emane_model_change(self, event: tk.Event): def emane_model_change(self, event: tk.Event) -> None:
""" """
update emane model options button update emane model options button
""" """
model_name = self.emane_model.get() model_name = self.emane_model.get()
self.emane_model_button.config(text=f"{model_name} options") self.emane_model_button.config(text=f"{model_name} options")
def click_apply(self): def click_apply(self) -> None:
self.node.emane = f"emane_{self.emane_model.get()}" self.node.emane = f"emane_{self.emane_model.get()}"
self.destroy() self.destroy()

View file

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

View file

@ -1,5 +1,5 @@
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
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
@ -13,9 +13,9 @@ if TYPE_CHECKING:
class ErrorDialog(Dialog): class ErrorDialog(Dialog):
def __init__(self, app: "Application", title: str, details: str) -> None: def __init__(self, app: "Application", title: str, details: str) -> None:
super().__init__(app, "CORE Exception") super().__init__(app, "CORE Exception")
self.title = title self.title: str = title
self.details = details self.details: str = details
self.error_message = None self.error_message: Optional[CodeText] = None
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:

View file

@ -1,7 +1,7 @@
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 typing import TYPE_CHECKING, Optional
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
@ -12,15 +12,15 @@ if TYPE_CHECKING:
class ExecutePythonDialog(Dialog): class ExecutePythonDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Execute Python Script") super().__init__(app, "Execute Python Script")
self.with_options = tk.IntVar(value=0) self.with_options: tk.IntVar = tk.IntVar(value=0)
self.options = tk.StringVar(value="") self.options: tk.StringVar = tk.StringVar(value="")
self.option_entry = None self.option_entry: Optional[ttk.Entry] = None
self.file_entry = None self.file_entry: Optional[ttk.Entry] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
i = 0 i = 0
frame = ttk.Frame(self.top, padding=FRAME_PAD) frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -63,13 +63,13 @@ class ExecutePythonDialog(Dialog):
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", padx=PADX) button.grid(row=0, column=1, sticky="ew", padx=PADX)
def add_options(self): def add_options(self) -> None:
if self.with_options.get(): if self.with_options.get():
self.option_entry.configure(state="normal") self.option_entry.configure(state="normal")
else: else:
self.option_entry.configure(state="disabled") self.option_entry.configure(state="disabled")
def select_file(self): def select_file(self) -> None:
file = filedialog.askopenfilename( file = filedialog.askopenfilename(
parent=self.top, parent=self.top,
initialdir=str(SCRIPT_PATH), initialdir=str(SCRIPT_PATH),
@ -80,7 +80,7 @@ class ExecutePythonDialog(Dialog):
self.file_entry.delete(0, "end") self.file_entry.delete(0, "end")
self.file_entry.insert("end", file) self.file_entry.insert("end", file)
def script_execute(self): def script_execute(self) -> None:
file = self.file_entry.get() file = self.file_entry.get()
options = self.option_entry.get() options = self.option_entry.get()
logging.info("Execute %s with options %s", file, options) logging.info("Execute %s with options %s", file, options)

View file

@ -1,7 +1,7 @@
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 from typing import TYPE_CHECKING, Optional
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
@ -13,8 +13,8 @@ if TYPE_CHECKING:
class FindDialog(Dialog): class FindDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Find", modal=False) super().__init__(app, "Find", modal=False)
self.find_text = tk.StringVar(value="") self.find_text: tk.StringVar = tk.StringVar(value="")
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.draw() self.draw()
self.protocol("WM_DELETE_WINDOW", self.close_dialog) self.protocol("WM_DELETE_WINDOW", self.close_dialog)
self.bind("<Return>", self.find_node) self.bind("<Return>", self.find_node)

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
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,15 +12,15 @@ if TYPE_CHECKING:
class HookDialog(Dialog): class HookDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application"): def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(app, "Hook", master=master) super().__init__(app, "Hook", master=master)
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.codetext = None self.codetext: Optional[CodeText] = None
self.hook = core_pb2.Hook() self.hook: core_pb2.Hook = core_pb2.Hook()
self.state = tk.StringVar() self.state: tk.StringVar = tk.StringVar()
self.draw() self.draw()
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)
@ -66,11 +66,11 @@ class HookDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def state_change(self, event: tk.Event): def state_change(self, event: tk.Event) -> None:
state_name = self.state.get() state_name = self.state.get()
self.name.set(f"{state_name.lower()}_hook.sh") self.name.set(f"{state_name.lower()}_hook.sh")
def set(self, hook: core_pb2.Hook): def set(self, hook: core_pb2.Hook) -> None:
self.hook = hook self.hook = hook
self.name.set(hook.file) self.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END) self.codetext.text.delete(1.0, tk.END)
@ -78,7 +78,7 @@ class HookDialog(Dialog):
state_name = core_pb2.SessionState.Enum.Name(hook.state) state_name = core_pb2.SessionState.Enum.Name(hook.state)
self.state.set(state_name) self.state.set(state_name)
def save(self): def save(self) -> None:
data = self.codetext.text.get("1.0", tk.END).strip() data = self.codetext.text.get("1.0", tk.END).strip()
state_value = core_pb2.SessionState.Enum.Value(self.state.get()) state_value = core_pb2.SessionState.Enum.Value(self.state.get())
self.hook.file = self.name.get() self.hook.file = self.name.get()
@ -88,15 +88,15 @@ class HookDialog(Dialog):
class HooksDialog(Dialog): class HooksDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Hooks") super().__init__(app, "Hooks")
self.listbox = None self.listbox: Optional[tk.Listbox] = None
self.edit_button = None self.edit_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.selected = None self.selected: Optional[str] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -124,7 +124,7 @@ class HooksDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
def click_create(self): def click_create(self) -> None:
dialog = HookDialog(self, self.app) dialog = HookDialog(self, self.app)
dialog.show() dialog.show()
hook = dialog.hook hook = dialog.hook
@ -132,19 +132,19 @@ class HooksDialog(Dialog):
self.app.core.hooks[hook.file] = hook self.app.core.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file) self.listbox.insert(tk.END, hook.file)
def click_edit(self): def click_edit(self) -> None:
hook = self.app.core.hooks[self.selected] hook = self.app.core.hooks[self.selected]
dialog = HookDialog(self, self.app) dialog = HookDialog(self, self.app)
dialog.set(hook) dialog.set(hook)
dialog.show() dialog.show()
def click_delete(self): def click_delete(self) -> None:
del self.app.core.hooks[self.selected] del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR) self.listbox.delete(tk.ANCHOR)
self.edit_button.config(state=tk.DISABLED) self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED)
def select(self, event: tk.Event): def select(self, event: tk.Event) -> None:
if self.listbox.curselection(): if self.listbox.curselection():
index = self.listbox.curselection()[0] index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index) self.selected = self.listbox.get(index)

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional
import netaddr import netaddr
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class IpConfigDialog(Dialog): class IpConfigDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "IP Configuration") super().__init__(app, "IP Configuration")
self.ip4 = self.app.guiconfig.ips.ip4 self.ip4: str = self.app.guiconfig.ips.ip4
self.ip6 = self.app.guiconfig.ips.ip6 self.ip6: str = self.app.guiconfig.ips.ip6
self.ip4s = self.app.guiconfig.ips.ip4s self.ip4s: List[str] = self.app.guiconfig.ips.ip4s
self.ip6s = self.app.guiconfig.ips.ip6s self.ip6s: List[str] = self.app.guiconfig.ips.ip6s
self.ip4_entry = None self.ip4_entry: Optional[ttk.Entry] = None
self.ip4_listbox = None self.ip4_listbox: Optional[ListboxScroll] = None
self.ip6_entry = None self.ip6_entry: Optional[ttk.Entry] = None
self.ip6_listbox = None self.ip6_listbox: Optional[ListboxScroll] = None
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -146,6 +146,6 @@ class IpConfigDialog(Dialog):
ip_config.ip6 = self.ip6 ip_config.ip6 = self.ip6
ip_config.ip4s = ip4s ip_config.ip4s = ip4s
ip_config.ip6s = ip6s ip_config.ip6s = ip6s
self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6) self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -3,7 +3,7 @@ link configuration
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.gui import validation from core.gui import validation
@ -16,7 +16,7 @@ if TYPE_CHECKING:
from core.gui.graph.graph import CanvasEdge from core.gui.graph.graph import CanvasEdge
def get_int(var: tk.StringVar) -> Union[int, None]: def get_int(var: tk.StringVar) -> Optional[int]:
value = var.get() value = var.get()
if value != "": if value != "":
return int(value) return int(value)
@ -24,7 +24,7 @@ def get_int(var: tk.StringVar) -> Union[int, None]:
return None return None
def get_float(var: tk.StringVar) -> Union[float, None]: def get_float(var: tk.StringVar) -> Optional[float]:
value = var.get() value = var.get()
if value != "": if value != "":
return float(value) return float(value)
@ -33,38 +33,39 @@ def get_float(var: tk.StringVar) -> Union[float, None]:
class LinkConfigurationDialog(Dialog): class LinkConfigurationDialog(Dialog):
def __init__(self, app: "Application", edge: "CanvasEdge"): def __init__(self, app: "Application", edge: "CanvasEdge") -> None:
super().__init__(app, "Link Configuration") super().__init__(app, "Link Configuration")
self.edge = edge self.edge: "CanvasEdge" = edge
self.is_symmetric = edge.link.options.unidirectional is False self.is_symmetric: bool = edge.link.options.unidirectional is False
if self.is_symmetric: if self.is_symmetric:
self.symmetry_var = tk.StringVar(value=">>") symmetry_var = tk.StringVar(value=">>")
else: else:
self.symmetry_var = tk.StringVar(value="<<") symmetry_var = tk.StringVar(value="<<")
self.symmetry_var: tk.StringVar = symmetry_var
self.bandwidth = tk.StringVar() self.bandwidth: tk.StringVar = tk.StringVar()
self.delay = tk.StringVar() self.delay: tk.StringVar = tk.StringVar()
self.jitter = tk.StringVar() self.jitter: tk.StringVar = tk.StringVar()
self.loss = tk.StringVar() self.loss: tk.StringVar = tk.StringVar()
self.duplicate = tk.StringVar() self.duplicate: tk.StringVar = tk.StringVar()
self.down_bandwidth = tk.StringVar() self.down_bandwidth: tk.StringVar = tk.StringVar()
self.down_delay = tk.StringVar() self.down_delay: tk.StringVar = tk.StringVar()
self.down_jitter = tk.StringVar() self.down_jitter: tk.StringVar = tk.StringVar()
self.down_loss = tk.StringVar() self.down_loss: tk.StringVar = tk.StringVar()
self.down_duplicate = tk.StringVar() self.down_duplicate: tk.StringVar = tk.StringVar()
self.color = tk.StringVar(value="#000000") self.color: tk.StringVar = tk.StringVar(value="#000000")
self.color_button = None self.color_button: Optional[tk.Button] = None
self.width = tk.DoubleVar() self.width: tk.DoubleVar = tk.DoubleVar()
self.load_link_config() self.load_link_config()
self.symmetric_frame = None self.symmetric_frame: Optional[ttk.Frame] = None
self.asymmetric_frame = None self.asymmetric_frame: Optional[ttk.Frame] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
source_name = self.app.canvas.nodes[self.edge.src].core_node.name source_name = self.app.canvas.nodes[self.edge.src].core_node.name
dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name
@ -207,13 +208,13 @@ class LinkConfigurationDialog(Dialog):
return frame return frame
def click_color(self): def click_color(self) -> None:
dialog = ColorPickerDialog(self, self.app, self.color.get()) dialog = ColorPickerDialog(self, self.app, self.color.get())
color = dialog.askcolor() color = dialog.askcolor()
self.color.set(color) self.color.set(color)
self.color_button.config(background=color) self.color_button.config(background=color)
def click_apply(self): def click_apply(self) -> None:
self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, width=self.width.get())
self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get())
link = self.edge.link link = self.edge.link
@ -223,25 +224,25 @@ class LinkConfigurationDialog(Dialog):
duplicate = get_int(self.duplicate) duplicate = get_int(self.duplicate)
loss = get_float(self.loss) loss = get_float(self.loss)
options = core_pb2.LinkOptions( options = core_pb2.LinkOptions(
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss
) )
link.options.CopyFrom(options) link.options.CopyFrom(options)
interface_one = None iface1_id = None
if link.HasField("interface_one"): if link.HasField("iface1"):
interface_one = link.interface_one.id iface1_id = link.iface1.id
interface_two = None iface2_id = None
if link.HasField("interface_two"): if link.HasField("iface2"):
interface_two = link.interface_two.id iface2_id = link.iface2.id
if not self.is_symmetric: if not self.is_symmetric:
link.options.unidirectional = True link.options.unidirectional = True
asym_interface_one = None asym_iface1 = None
if interface_one: if iface1_id:
asym_interface_one = core_pb2.Interface(id=interface_one) asym_iface1 = core_pb2.Interface(id=iface1_id)
asym_interface_two = None asym_iface2 = None
if interface_two: if iface2_id:
asym_interface_two = core_pb2.Interface(id=interface_two) asym_iface2 = core_pb2.Interface(id=iface2_id)
down_bandwidth = get_int(self.down_bandwidth) down_bandwidth = get_int(self.down_bandwidth)
down_jitter = get_int(self.down_jitter) down_jitter = get_int(self.down_jitter)
down_delay = get_int(self.down_delay) down_delay = get_int(self.down_delay)
@ -252,14 +253,14 @@ class LinkConfigurationDialog(Dialog):
jitter=down_jitter, jitter=down_jitter,
delay=down_delay, delay=down_delay,
dup=down_duplicate, dup=down_duplicate,
per=down_loss, loss=down_loss,
unidirectional=True, unidirectional=True,
) )
self.edge.asymmetric_link = core_pb2.Link( self.edge.asymmetric_link = core_pb2.Link(
node_one_id=link.node_two_id, node1_id=link.node2_id,
node_two_id=link.node_one_id, node2_id=link.node1_id,
interface_one=asym_interface_one, iface1=asym_iface1,
interface_two=asym_interface_two, iface2=asym_iface2,
options=options, options=options,
) )
else: else:
@ -270,25 +271,27 @@ class LinkConfigurationDialog(Dialog):
session_id = self.app.core.session_id session_id = self.app.core.session_id
self.app.core.client.edit_link( self.app.core.client.edit_link(
session_id, session_id,
link.node_one_id, link.node1_id,
link.node_two_id, link.node2_id,
link.options, link.options,
interface_one, iface1_id,
interface_two, iface2_id,
) )
if self.edge.asymmetric_link: if self.edge.asymmetric_link:
self.app.core.client.edit_link( self.app.core.client.edit_link(
session_id, session_id,
link.node_two_id, link.node2_id,
link.node_one_id, link.node1_id,
self.edge.asymmetric_link.options, self.edge.asymmetric_link.options,
interface_one, iface1_id,
interface_two, iface2_id,
) )
# update edge label
self.edge.draw_link_options()
self.destroy() self.destroy()
def change_symmetry(self): def change_symmetry(self) -> None:
if self.is_symmetric: if self.is_symmetric:
self.is_symmetric = False self.is_symmetric = False
self.symmetry_var.set("<<") self.symmetry_var.set("<<")
@ -304,7 +307,7 @@ class LinkConfigurationDialog(Dialog):
self.asymmetric_frame.grid_forget() self.asymmetric_frame.grid_forget()
self.symmetric_frame.grid(row=2, column=0) self.symmetric_frame.grid(row=2, column=0)
def load_link_config(self): def load_link_config(self) -> None:
""" """
populate link config to the table populate link config to the table
""" """
@ -317,12 +320,12 @@ class LinkConfigurationDialog(Dialog):
self.bandwidth.set(str(link.options.bandwidth)) self.bandwidth.set(str(link.options.bandwidth))
self.jitter.set(str(link.options.jitter)) self.jitter.set(str(link.options.jitter))
self.duplicate.set(str(link.options.dup)) self.duplicate.set(str(link.options.dup))
self.loss.set(str(link.options.per)) self.loss.set(str(link.options.loss))
self.delay.set(str(link.options.delay)) self.delay.set(str(link.options.delay))
if not self.is_symmetric: if not self.is_symmetric:
asym_link = self.edge.asymmetric_link asym_link = self.edge.asymmetric_link
self.down_bandwidth.set(str(asym_link.options.bandwidth)) self.down_bandwidth.set(str(asym_link.options.bandwidth))
self.down_jitter.set(str(asym_link.options.jitter)) self.down_jitter.set(str(asym_link.options.jitter))
self.down_duplicate.set(str(asym_link.options.dup)) self.down_duplicate.set(str(asym_link.options.dup))
self.down_loss.set(str(asym_link.options.per)) self.down_loss.set(str(asym_link.options.loss))
self.down_delay.set(str(asym_link.options.delay)) self.down_delay.set(str(asym_link.options.delay))

View file

@ -15,7 +15,7 @@ class MacConfigDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "MAC Configuration") super().__init__(app, "MAC Configuration")
mac = self.app.guiconfig.mac mac = self.app.guiconfig.mac
self.mac_var = tk.StringVar(value=mac) self.mac_var: tk.StringVar = tk.StringVar(value=mac)
self.draw() self.draw()
def draw(self) -> None: def draw(self) -> None:
@ -55,7 +55,7 @@ class MacConfigDialog(Dialog):
if not netaddr.valid_mac(mac): if not netaddr.valid_mac(mac):
messagebox.showerror("MAC Error", f"{mac} is an invalid mac") messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
else: else:
self.app.core.interfaces_manager.mac = netaddr.EUI(mac) self.app.core.ifaces_manager.mac = netaddr.EUI(mac)
self.app.guiconfig.mac = mac self.app.guiconfig.mac = mac
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()

View file

@ -2,10 +2,12 @@
mobility configuration mobility configuration
""" """
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
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 ConfigFrame from core.gui.widgets import ConfigFrame
@ -16,23 +18,24 @@ if TYPE_CHECKING:
class MobilityConfigDialog(Dialog): class MobilityConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration") super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.has_error: bool = False
try: try:
self.config = self.canvas_node.mobility_config config = self.canvas_node.mobility_config
if not self.config: if not config:
self.config = self.app.core.get_mobility_config(self.node.id) config = self.app.core.get_mobility_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
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.app.show_grpc_exception("Get Mobility Config Error", e)
self.has_error = True self.has_error: bool = 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(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config)
@ -40,7 +43,7 @@ class MobilityConfigDialog(Dialog):
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -52,7 +55,7 @@ class MobilityConfigDialog(Dialog):
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_apply(self): def click_apply(self) -> None:
self.config_frame.parse_config() self.config_frame.parse_config()
self.canvas_node.mobility_config = self.config self.canvas_node.mobility_config = self.config
self.destroy() self.destroy()

View file

@ -1,9 +1,11 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
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.images import ImageEnum from core.gui.images import ImageEnum
@ -13,18 +15,23 @@ if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode from core.gui.graph.node import CanvasNode
ICON_SIZE = 16 ICON_SIZE: int = 16
class MobilityPlayer: class MobilityPlayer:
def __init__(self, app: "Application", canvas_node: "CanvasNode", config): def __init__(
self.app = app self,
self.canvas_node = canvas_node app: "Application",
self.config = config canvas_node: "CanvasNode",
self.dialog = None config: Dict[str, ConfigOption],
self.state = None ) -> None:
self.app: "Application" = app
self.canvas_node: "CanvasNode" = canvas_node
self.config: Dict[str, ConfigOption] = config
self.dialog: Optional[MobilityPlayerDialog] = None
self.state: Optional[MobilityAction] = None
def show(self): def show(self) -> None:
if self.dialog: if self.dialog:
self.dialog.destroy() self.dialog.destroy()
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config) self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
@ -37,44 +44,49 @@ class MobilityPlayer:
self.set_stop() self.set_stop()
self.dialog.show() self.dialog.show()
def close(self): def close(self) -> None:
if self.dialog: if self.dialog:
self.dialog.destroy() self.dialog.destroy()
self.dialog = None self.dialog = None
def set_play(self): def set_play(self) -> None:
self.state = MobilityAction.START self.state = MobilityAction.START
if self.dialog: if self.dialog:
self.dialog.set_play() self.dialog.set_play()
def set_pause(self): def set_pause(self) -> None:
self.state = MobilityAction.PAUSE self.state = MobilityAction.PAUSE
if self.dialog: if self.dialog:
self.dialog.set_pause() self.dialog.set_pause()
def set_stop(self): def set_stop(self) -> None:
self.state = MobilityAction.STOP self.state = MobilityAction.STOP
if self.dialog: if self.dialog:
self.dialog.set_stop() self.dialog.set_stop()
class MobilityPlayerDialog(Dialog): class MobilityPlayerDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode", config): def __init__(
self,
app: "Application",
canvas_node: "CanvasNode",
config: Dict[str, ConfigOption],
) -> None:
super().__init__( super().__init__(
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("")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.config = config self.config: Dict[str, ConfigOption] = config
self.play_button = None self.play_button: Optional[ttk.Button] = None
self.pause_button = None self.pause_button: Optional[ttk.Button] = None
self.stop_button = None self.stop_button: Optional[ttk.Button] = None
self.progressbar = None self.progressbar: Optional[ttk.Progressbar] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
file_name = self.config["file"].value file_name = self.config["file"].value
@ -114,27 +126,27 @@ class MobilityPlayerDialog(Dialog):
label = ttk.Label(frame, text=f"rate {rate} ms") label = ttk.Label(frame, text=f"rate {rate} ms")
label.grid(row=0, column=4) label.grid(row=0, column=4)
def clear_buttons(self): def clear_buttons(self) -> None:
self.play_button.state(["!pressed"]) self.play_button.state(["!pressed"])
self.pause_button.state(["!pressed"]) self.pause_button.state(["!pressed"])
self.stop_button.state(["!pressed"]) self.stop_button.state(["!pressed"])
def set_play(self): def set_play(self) -> None:
self.clear_buttons() self.clear_buttons()
self.play_button.state(["pressed"]) self.play_button.state(["pressed"])
self.progressbar.start() self.progressbar.start()
def set_pause(self): def set_pause(self) -> None:
self.clear_buttons() self.clear_buttons()
self.pause_button.state(["pressed"]) self.pause_button.state(["pressed"])
self.progressbar.stop() self.progressbar.stop()
def set_stop(self): def set_stop(self) -> None:
self.clear_buttons() self.clear_buttons()
self.stop_button.state(["pressed"]) self.stop_button.state(["pressed"])
self.progressbar.stop() self.progressbar.stop()
def click_play(self): def click_play(self) -> None:
self.set_play() self.set_play()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:
@ -144,7 +156,7 @@ class MobilityPlayerDialog(Dialog):
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
def click_pause(self): def click_pause(self) -> None:
self.set_pause() self.set_pause()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:
@ -154,7 +166,7 @@ class MobilityPlayerDialog(Dialog):
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e) self.app.show_grpc_exception("Mobility Error", e)
def click_stop(self): def click_stop(self) -> None:
self.set_stop() self.set_stop()
session_id = self.app.core.session_id session_id = self.app.core.session_id
try: try:

View file

@ -2,10 +2,12 @@ import logging
import tkinter as tk import tkinter as tk
from functools import partial from functools import partial
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import netaddr import netaddr
from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import Node
from core.gui import nodeutils, validation 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
@ -86,35 +88,35 @@ class InterfaceData:
mac: tk.StringVar, mac: tk.StringVar,
ip4: tk.StringVar, ip4: tk.StringVar,
ip6: tk.StringVar, ip6: tk.StringVar,
): ) -> None:
self.is_auto = is_auto self.is_auto: tk.BooleanVar = is_auto
self.mac = mac self.mac: tk.StringVar = mac
self.ip4 = ip4 self.ip4: tk.StringVar = ip4
self.ip6 = ip6 self.ip6: tk.StringVar = ip6
class NodeConfigDialog(Dialog): class NodeConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
""" """
create an instance of node configuration create an instance of node configuration
""" """
super().__init__(app, f"{canvas_node.core_node.name} Configuration") super().__init__(app, f"{canvas_node.core_node.name} Configuration")
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node = canvas_node.core_node self.node: Node = canvas_node.core_node
self.image = canvas_node.image self.image: PhotoImage = canvas_node.image
self.image_file = None self.image_file: Optional[str] = None
self.image_button = None self.image_button: Optional[ttk.Button] = None
self.name = tk.StringVar(value=self.node.name) self.name: tk.StringVar = tk.StringVar(value=self.node.name)
self.type = tk.StringVar(value=self.node.model) self.type: tk.StringVar = tk.StringVar(value=self.node.model)
self.container_image = tk.StringVar(value=self.node.image) self.container_image: tk.StringVar = tk.StringVar(value=self.node.image)
server = "localhost" server = "localhost"
if self.node.server: if self.node.server:
server = self.node.server server = self.node.server
self.server = tk.StringVar(value=server) self.server: tk.StringVar = tk.StringVar(value=server)
self.interfaces = {} self.ifaces: Dict[int, InterfaceData] = {}
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
row = 0 row = 0
@ -183,53 +185,53 @@ class NodeConfigDialog(Dialog):
row += 1 row += 1
if NodeUtils.is_rj45_node(self.node.type): if NodeUtils.is_rj45_node(self.node.type):
response = self.app.core.client.get_interfaces() response = self.app.core.client.get_ifaces()
logging.debug("host machine available interfaces: %s", response) logging.debug("host machine available interfaces: %s", response)
interfaces = ListboxScroll(frame) ifaces = ListboxScroll(frame)
interfaces.listbox.config(state=state) ifaces.listbox.config(state=state)
interfaces.grid( ifaces.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
) )
for inf in sorted(response.interfaces[:]): for inf in sorted(response.ifaces[:]):
interfaces.listbox.insert(tk.END, inf) ifaces.listbox.insert(tk.END, inf)
row += 1 row += 1
interfaces.listbox.bind("<<ListboxSelect>>", self.interface_select) ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select)
# interfaces # interfaces
if self.canvas_node.interfaces: if self.canvas_node.ifaces:
self.draw_interfaces() self.draw_ifaces()
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_interfaces(self): def draw_ifaces(self) -> None:
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 state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
for interface_id in sorted(self.canvas_node.interfaces): for iface_id in sorted(self.canvas_node.ifaces):
interface = self.canvas_node.interfaces[interface_id] iface = self.canvas_node.ifaces[iface_id]
tab = ttk.Frame(notebook, padding=FRAME_PAD) tab = ttk.Frame(notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew", pady=PADY) tab.grid(sticky="nsew", pady=PADY)
tab.columnconfigure(1, weight=1) tab.columnconfigure(1, weight=1)
tab.columnconfigure(2, weight=1) tab.columnconfigure(2, weight=1)
notebook.add(tab, text=interface.name) notebook.add(tab, text=iface.name)
row = 0 row = 0
emane_node = self.canvas_node.has_emane_link(interface.id) emane_node = self.canvas_node.has_emane_link(iface.id)
if emane_node: if emane_node:
emane_model = emane_node.emane.split("_")[1] emane_model = emane_node.emane.split("_")[1]
button = ttk.Button( button = ttk.Button(
tab, tab,
text=f"Configure EMANE {emane_model}", text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, interface.id), command=lambda: self.click_emane_config(emane_model, iface.id),
) )
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
row += 1 row += 1
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 iface.mac
mac_state = tk.DISABLED if auto_set else tk.NORMAL mac_state = tk.DISABLED if auto_set else tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set) is_auto = tk.BooleanVar(value=auto_set)
checkbutton = ttk.Checkbutton( checkbutton = ttk.Checkbutton(
@ -237,7 +239,7 @@ class NodeConfigDialog(Dialog):
) )
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=iface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=mac_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, mac) func = partial(mac_auto, is_auto, entry, mac)
@ -247,8 +249,8 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="IPv4") label = ttk.Label(tab, text="IPv4")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip4_net = "" ip4_net = ""
if interface.ip4: if iface.ip4:
ip4_net = f"{interface.ip4}/{interface.ip4mask}" ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
ip4 = tk.StringVar(value=ip4_net) ip4 = tk.StringVar(value=ip4_net)
entry = ttk.Entry(tab, textvariable=ip4, state=state) 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")
@ -257,15 +259,15 @@ class NodeConfigDialog(Dialog):
label = ttk.Label(tab, text="IPv6") label = ttk.Label(tab, text="IPv6")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
ip6_net = "" ip6_net = ""
if interface.ip6: if iface.ip6:
ip6_net = f"{interface.ip6}/{interface.ip6mask}" ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
ip6 = tk.StringVar(value=ip6_net) ip6 = tk.StringVar(value=ip6_net)
entry = ttk.Entry(tab, textvariable=ip6, state=state) 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.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -277,20 +279,20 @@ class NodeConfigDialog(Dialog):
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, iface_id: int) -> None:
dialog = EmaneModelDialog( dialog = EmaneModelDialog(
self, self.app, self.canvas_node, emane_model, interface_id self, self.app, self.canvas_node, emane_model, iface_id
) )
dialog.show() dialog.show()
def click_icon(self): def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH) file_path = image_chooser(self, ICONS_PATH)
if file_path: if file_path:
self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image = Images.create(file_path, nodeutils.ICON_SIZE)
self.image_button.config(image=self.image) self.image_button.config(image=self.image)
self.image_file = file_path self.image_file = file_path
def click_apply(self): def click_apply(self) -> None:
error = False error = False
# update core node # update core node
@ -309,54 +311,54 @@ class NodeConfigDialog(Dialog):
self.canvas_node.image = self.image self.canvas_node.image = self.image
# update node interface data # update node interface data
for interface in self.canvas_node.interfaces.values(): for iface in self.canvas_node.ifaces.values():
data = self.interfaces[interface.id] data = self.ifaces[iface.id]
# validate ip4 # validate ip4
ip4_net = data.ip4.get() ip4_net = data.ip4.get()
if not check_ip4(self, interface.name, ip4_net): if not check_ip4(self, iface.name, ip4_net):
error = True error = True
break break
if ip4_net: if ip4_net:
ip4, ip4mask = ip4_net.split("/") ip4, ip4_mask = ip4_net.split("/")
ip4mask = int(ip4mask) ip4_mask = int(ip4_mask)
else: else:
ip4, ip4mask = "", 0 ip4, ip4_mask = "", 0
interface.ip4 = ip4 iface.ip4 = ip4
interface.ip4mask = ip4mask iface.ip4_mask = ip4_mask
# validate ip6 # validate ip6
ip6_net = data.ip6.get() ip6_net = data.ip6.get()
if not check_ip6(self, interface.name, ip6_net): if not check_ip6(self, iface.name, ip6_net):
error = True error = True
break break
if ip6_net: if ip6_net:
ip6, ip6mask = ip6_net.split("/") ip6, ip6_mask = ip6_net.split("/")
ip6mask = int(ip6mask) ip6_mask = int(ip6_mask)
else: else:
ip6, ip6mask = "", 0 ip6, ip6_mask = "", 0
interface.ip6 = ip6 iface.ip6 = ip6
interface.ip6mask = ip6mask iface.ip6_mask = ip6_mask
mac = data.mac.get() mac = data.mac.get()
auto_mac = data.is_auto.get() auto_mac = data.is_auto.get()
if not auto_mac and not netaddr.valid_mac(mac): if not auto_mac and not netaddr.valid_mac(mac):
title = f"MAC Error for {interface.name}" title = f"MAC Error for {iface.name}"
messagebox.showerror(title, "Invalid MAC Address") messagebox.showerror(title, "Invalid MAC Address")
error = True error = True
break break
elif not auto_mac: elif not auto_mac:
mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
interface.mac = str(mac) iface.mac = str(mac)
# redraw # redraw
if not error: if not error:
self.canvas_node.redraw() self.canvas_node.redraw()
self.destroy() self.destroy()
def interface_select(self, event: tk.Event): def iface_select(self, event: tk.Event) -> None:
listbox = event.widget listbox = event.widget
cur = listbox.curselection() cur = listbox.curselection()
if cur: if cur:
interface = listbox.get(cur[0]) iface = listbox.get(cur[0])
self.name.set(interface) self.name.set(iface)

View file

@ -4,7 +4,7 @@ core node services
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING, Set from typing import TYPE_CHECKING, Optional, 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
@ -19,20 +19,20 @@ if TYPE_CHECKING:
class NodeConfigServiceDialog(Dialog): class NodeConfigServiceDialog(Dialog):
def __init__( def __init__(
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
): ) -> None:
title = f"{canvas_node.core_node.name} Config Services" title = f"{canvas_node.core_node.name} Config Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = canvas_node.core_node.id self.node_id: int = canvas_node.core_node.id
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
if services is None: if services is None:
services = set(canvas_node.core_node.config_services) services = set(canvas_node.core_node.config_services)
self.current_services = services self.current_services: Set[str] = services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -84,9 +84,9 @@ class NodeConfigServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.handle_group_change()
def handle_group_change(self, event: tk.Event = None): def handle_group_change(self, event: tk.Event = None) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -96,7 +96,7 @@ class NodeConfigServiceDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.IntVar): def service_clicked(self, name: str, var: tk.IntVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
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:
@ -104,7 +104,7 @@ class NodeConfigServiceDialog(Dialog):
self.draw_current_services() self.draw_current_services()
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) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
if len(current_selection): if len(current_selection):
dialog = ConfigServiceConfigDialog( dialog = ConfigServiceConfigDialog(
@ -124,25 +124,25 @@ class NodeConfigServiceDialog(Dialog):
parent=self, parent=self,
) )
def draw_current_services(self): def draw_current_services(self) -> None:
self.current.listbox.delete(0, tk.END) self.current.listbox.delete(0, tk.END)
for name in sorted(self.current_services): for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name): if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
def click_save(self): def click_save(self) -> None:
self.canvas_node.core_node.config_services[:] = self.current_services self.canvas_node.core_node.config_services[:] = self.current_services
logging.info( logging.info(
"saved node config services: %s", self.canvas_node.core_node.config_services "saved node config services: %s", self.canvas_node.core_node.config_services
) )
self.destroy() self.destroy()
def click_cancel(self): def click_cancel(self) -> None:
self.current_services = None self.current_services = None
self.destroy() self.destroy()
def click_remove(self): def click_remove(self) -> None:
cur = self.current.listbox.curselection() cur = self.current.listbox.curselection()
if cur: if cur:
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])

View file

@ -3,7 +3,7 @@ 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 from typing import TYPE_CHECKING, Optional, Set
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
@ -16,19 +16,19 @@ if TYPE_CHECKING:
class NodeServiceDialog(Dialog): class NodeServiceDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
title = f"{canvas_node.core_node.name} Services" title = f"{canvas_node.core_node.name} Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = canvas_node.core_node.id self.node_id: int = canvas_node.core_node.id
self.groups = None self.groups: Optional[ListboxScroll] = None
self.services = None self.services: Optional[CheckboxList] = None
self.current = None self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services) services = set(canvas_node.core_node.services)
self.current_services = services self.current_services: Set[str] = services
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -82,9 +82,9 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew") button.grid(row=0, column=3, sticky="ew")
# trigger group change # trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>") self.handle_group_change()
def handle_group_change(self, event: tk.Event = None): def handle_group_change(self, event: tk.Event = None) -> None:
selection = self.groups.listbox.curselection() selection = self.groups.listbox.curselection()
if selection: if selection:
index = selection[0] index = selection[0]
@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog):
checked = name in self.current_services checked = name in self.current_services
self.services.add(name, checked) self.services.add(name, checked)
def service_clicked(self, name: str, var: tk.IntVar): def service_clicked(self, name: str, var: tk.IntVar) -> None:
if var.get() and name not in self.current_services: if var.get() and name not in self.current_services:
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:
@ -106,7 +106,7 @@ class NodeServiceDialog(Dialog):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
self.canvas_node.core_node.services[:] = self.current_services self.canvas_node.core_node.services[:] = self.current_services
def click_configure(self): def click_configure(self) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
if len(current_selection): if len(current_selection):
dialog = ServiceConfigDialog( dialog = ServiceConfigDialog(
@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog):
"Service Configuration", "Select a service to configure", parent=self "Service Configuration", "Select a service to configure", parent=self
) )
def click_save(self): def click_save(self) -> None:
core_node = self.canvas_node.core_node core_node = self.canvas_node.core_node
core_node.services[:] = self.current_services core_node.services[:] = self.current_services
self.destroy() self.destroy()
def click_remove(self): def click_remove(self) -> None:
cur = self.current.listbox.curselection() cur = self.current.listbox.curselection()
if cur: if cur:
service = self.current.listbox.get(cur[0]) service = self.current.listbox.get(cur[0])

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import Observer from core.gui.appconfig import Observer
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -12,18 +12,18 @@ if TYPE_CHECKING:
class ObserverDialog(Dialog): class ObserverDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Observer Widgets") super().__init__(app, "Observer Widgets")
self.observers = None self.observers: Optional[tk.Listbox] = None
self.save_button = None self.save_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.selected = None self.selected: Optional[str] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.name = tk.StringVar() self.name: tk.StringVar = tk.StringVar()
self.cmd = tk.StringVar() self.cmd: tk.StringVar = tk.StringVar()
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_listbox() self.draw_listbox()
@ -31,7 +31,7 @@ class ObserverDialog(Dialog):
self.draw_config_buttons() self.draw_config_buttons()
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_listbox(self): def draw_listbox(self) -> None:
listbox_scroll = ListboxScroll(self.top) listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY) listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.columnconfigure(0, weight=1)
@ -42,7 +42,7 @@ class ObserverDialog(Dialog):
for name in sorted(self.app.core.custom_observers): for name in sorted(self.app.core.custom_observers):
self.observers.insert(tk.END, name) self.observers.insert(tk.END, name)
def draw_form_fields(self): def draw_form_fields(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -57,7 +57,7 @@ class ObserverDialog(Dialog):
entry = ttk.Entry(frame, textvariable=self.cmd) entry = ttk.Entry(frame, textvariable=self.cmd)
entry.grid(row=1, column=1, sticky="ew") entry.grid(row=1, column=1, sticky="ew")
def draw_config_buttons(self): def draw_config_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY) frame.grid(sticky="ew", pady=PADY)
for i in range(3): for i in range(3):
@ -76,7 +76,7 @@ class ObserverDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -88,14 +88,14 @@ class ObserverDialog(Dialog):
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_save_config(self): def click_save_config(self) -> None:
self.app.guiconfig.observers.clear() self.app.guiconfig.observers.clear()
for observer in self.app.core.custom_observers.values(): for observer in self.app.core.custom_observers.values():
self.app.guiconfig.observers.append(observer) self.app.guiconfig.observers.append(observer)
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
name = self.name.get() name = self.name.get()
if name not in self.app.core.custom_observers: if name not in self.app.core.custom_observers:
cmd = self.cmd.get() cmd = self.cmd.get()
@ -109,7 +109,7 @@ class ObserverDialog(Dialog):
else: else:
messagebox.showerror("Observer Error", f"{name} already exists") messagebox.showerror("Observer Error", f"{name} already exists")
def click_save(self): def click_save(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -122,7 +122,7 @@ class ObserverDialog(Dialog):
self.observers.insert(self.selected_index, name) self.observers.insert(self.selected_index, name)
self.observers.selection_set(self.selected_index) self.observers.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected: if self.selected:
self.observers.delete(self.selected_index) self.observers.delete(self.selected_index)
del self.app.core.custom_observers[self.selected] del self.app.core.custom_observers[self.selected]
@ -136,7 +136,7 @@ class ObserverDialog(Dialog):
self.app.menubar.observers_menu.draw_custom() self.app.menubar.observers_menu.draw_custom()
self.app.toolbar.observers_menu.draw_custom() self.app.toolbar.observers_menu.draw_custom()
def handle_observer_change(self, event: tk.Event): def handle_observer_change(self, event: tk.Event) -> None:
selection = self.observers.curselection() selection = self.observers.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -12,27 +12,27 @@ from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
SCALE_INTERVAL = 0.01 SCALE_INTERVAL: float = 0.01
class PreferencesDialog(Dialog): class PreferencesDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Preferences") super().__init__(app, "Preferences")
self.gui_scale = tk.DoubleVar(value=self.app.app_scale) self.gui_scale: tk.DoubleVar = 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 = tk.StringVar(value=preferences.editor)
self.theme = tk.StringVar(value=preferences.theme) self.theme: tk.StringVar = tk.StringVar(value=preferences.theme)
self.terminal = tk.StringVar(value=preferences.terminal) self.terminal: tk.StringVar = tk.StringVar(value=preferences.terminal)
self.gui3d = tk.StringVar(value=preferences.gui3d) self.gui3d: tk.StringVar = tk.StringVar(value=preferences.gui3d)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_preferences() self.draw_preferences()
self.draw_buttons() self.draw_buttons()
def draw_preferences(self): def draw_preferences(self) -> None:
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -88,7 +88,7 @@ class PreferencesDialog(Dialog):
scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale) scrollbar = ttk.Scrollbar(scale_frame, command=self.adjust_scale)
scrollbar.grid(row=0, column=2) scrollbar.grid(row=0, column=2)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -100,12 +100,12 @@ class PreferencesDialog(Dialog):
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 theme_change(self, event: tk.Event): def theme_change(self, event: tk.Event) -> None:
theme = self.theme.get() theme = self.theme.get()
logging.info("changing theme: %s", theme) logging.info("changing theme: %s", theme)
self.app.style.theme_use(theme) self.app.style.theme_use(theme)
def click_save(self): def click_save(self) -> None:
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()
@ -118,7 +118,7 @@ class PreferencesDialog(Dialog):
self.scale_adjust() self.scale_adjust()
self.destroy() self.destroy()
def scale_adjust(self): def scale_adjust(self) -> None:
app_scale = self.gui_scale.get() app_scale = self.gui_scale.get()
self.app.app_scale = app_scale self.app.app_scale = app_scale
self.app.master.tk.call("tk", "scaling", app_scale) self.app.master.tk.call("tk", "scaling", app_scale)
@ -136,7 +136,7 @@ class PreferencesDialog(Dialog):
self.app.toolbar.scale() self.app.toolbar.scale()
self.app.canvas.scale_graph() self.app.canvas.scale_graph()
def adjust_scale(self, arg1: str, arg2: str, arg3: str): def adjust_scale(self, arg1: str, arg2: str, arg3: str) -> None:
scale_value = self.gui_scale.get() scale_value = self.gui_scale.get()
if arg2 == "-1": if arg2 == "-1":
if scale_value <= LARGEST_SCALE - SCALE_INTERVAL: if scale_value <= LARGEST_SCALE - SCALE_INTERVAL:

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
@ -14,10 +14,10 @@ if TYPE_CHECKING:
class RunToolDialog(Dialog): class RunToolDialog(Dialog):
def __init__(self, app: "Application") -> None: def __init__(self, app: "Application") -> None:
super().__init__(app, "Run Tool") super().__init__(app, "Run Tool")
self.cmd = tk.StringVar(value="ps ax") self.cmd: tk.StringVar = tk.StringVar(value="ps ax")
self.result = None self.result: Optional[CodeText] = None
self.node_list = None self.node_list: Optional[ListboxScroll] = None
self.executable_nodes = {} self.executable_nodes: Dict[str, int] = {}
self.store_nodes() self.store_nodes()
self.draw() self.draw()

View file

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import CoreServer from core.gui.appconfig import CoreServer
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -10,24 +10,24 @@ from core.gui.widgets import ListboxScroll
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
DEFAULT_NAME = "example" DEFAULT_NAME: str = "example"
DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_ADDRESS: str = "127.0.0.1"
DEFAULT_PORT = 50051 DEFAULT_PORT: int = 50051
class ServersDialog(Dialog): class ServersDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "CORE Servers") super().__init__(app, "CORE Servers")
self.name = tk.StringVar(value=DEFAULT_NAME) self.name: tk.StringVar = tk.StringVar(value=DEFAULT_NAME)
self.address = tk.StringVar(value=DEFAULT_ADDRESS) self.address: tk.StringVar = tk.StringVar(value=DEFAULT_ADDRESS)
self.servers = None self.servers: Optional[tk.Listbox] = None
self.selected_index = None self.selected_index: Optional[int] = None
self.selected = None self.selected: Optional[str] = None
self.save_button = None self.save_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.draw_servers() self.draw_servers()
@ -35,7 +35,7 @@ class ServersDialog(Dialog):
self.draw_server_configuration() self.draw_server_configuration()
self.draw_apply_buttons() self.draw_apply_buttons()
def draw_servers(self): def draw_servers(self) -> None:
listbox_scroll = ListboxScroll(self.top) listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(pady=PADY, sticky="nsew") listbox_scroll.grid(pady=PADY, sticky="nsew")
listbox_scroll.columnconfigure(0, weight=1) listbox_scroll.columnconfigure(0, weight=1)
@ -48,7 +48,7 @@ class ServersDialog(Dialog):
for server in self.app.core.servers: for server in self.app.core.servers:
self.servers.insert(tk.END, server) self.servers.insert(tk.END, server)
def draw_server_configuration(self): def draw_server_configuration(self) -> None:
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
@ -64,7 +64,7 @@ class ServersDialog(Dialog):
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")
def draw_servers_buttons(self): def draw_servers_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew") frame.grid(pady=PADY, sticky="ew")
for i in range(3): for i in range(3):
@ -83,7 +83,7 @@ class ServersDialog(Dialog):
) )
self.delete_button.grid(row=0, column=2, sticky="ew") self.delete_button.grid(row=0, column=2, sticky="ew")
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
@ -104,7 +104,7 @@ class ServersDialog(Dialog):
self.app.save_config() self.app.save_config()
self.destroy() self.destroy()
def click_create(self): def click_create(self) -> None:
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()
@ -112,7 +112,7 @@ class ServersDialog(Dialog):
self.app.core.servers[name] = server self.app.core.servers[name] = server
self.servers.insert(tk.END, name) self.servers.insert(tk.END, name)
def click_save(self): def click_save(self) -> None:
name = self.name.get() name = self.name.get()
if self.selected: if self.selected:
previous_name = self.selected previous_name = self.selected
@ -125,7 +125,7 @@ class ServersDialog(Dialog):
self.servers.insert(self.selected_index, name) self.servers.insert(self.selected_index, name)
self.servers.selection_set(self.selected_index) self.servers.selection_set(self.selected_index)
def click_delete(self): def click_delete(self) -> None:
if self.selected: if self.selected:
self.servers.delete(self.selected_index) self.servers.delete(self.selected_index)
del self.app.core.servers[self.selected] del self.app.core.servers[self.selected]
@ -137,7 +137,7 @@ class ServersDialog(Dialog):
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)
def handle_server_change(self, event: tk.Event): def handle_server_change(self, event: tk.Event) -> None:
selection = self.servers.curselection() selection = self.servers.curselection()
if selection: if selection:
self.selected_index = selection[0] self.selected_index = selection[0]

View file

@ -2,11 +2,12 @@ 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, List from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc.services_pb2 import ServiceValidationMode from core.api.grpc.services_pb2 import NodeServiceData, 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.images import ImageEnum, Images from core.gui.images import ImageEnum, Images
@ -16,8 +17,9 @@ 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 from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
ICON_SIZE = 16 ICON_SIZE: int = 16
class ServiceConfigDialog(Dialog): class ServiceConfigDialog(Dialog):
@ -28,54 +30,57 @@ class ServiceConfigDialog(Dialog):
service_name: str, service_name: str,
canvas_node: "CanvasNode", canvas_node: "CanvasNode",
node_id: int, node_id: int,
): ) -> None:
title = f"{service_name} Service" title = f"{service_name} Service"
super().__init__(app, title, master=master) super().__init__(app, title, master=master)
self.core = app.core self.core: "CoreClient" = app.core
self.canvas_node = canvas_node self.canvas_node: "CanvasNode" = canvas_node
self.node_id = node_id self.node_id: int = node_id
self.service_name = service_name self.service_name: str = service_name
self.radiovar = tk.IntVar() self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.radiovar.set(2) self.metadata: str = ""
self.metadata = "" self.filenames: List[str] = []
self.filenames = [] self.dependencies: List[str] = []
self.dependencies = [] self.executables: List[str] = []
self.executables = [] self.startup_commands: List[str] = []
self.startup_commands = [] self.validation_commands: List[str] = []
self.validation_commands = [] self.shutdown_commands: List[str] = []
self.shutdown_commands = [] self.default_startup: List[str] = []
self.default_startup = [] self.default_validate: List[str] = []
self.default_validate = [] self.default_shutdown: List[str] = []
self.default_shutdown = [] self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_mode = None self.validation_time: Optional[int] = None
self.validation_time = None self.validation_period: Optional[float] = None
self.validation_period = None self.directory_entry: Optional[ttk.Entry] = None
self.directory_entry = None self.default_directories: List[str] = []
self.default_directories = [] self.temp_directories: List[str] = []
self.temp_directories = [] self.documentnew_img: PhotoImage = self.app.get_icon(
self.documentnew_img = self.app.get_icon(ImageEnum.DOCUMENTNEW, ICON_SIZE) ImageEnum.DOCUMENTNEW, ICON_SIZE
self.editdelete_img = self.app.get_icon(ImageEnum.EDITDELETE, ICON_SIZE) )
self.notebook = None self.editdelete_img: PhotoImage = self.app.get_icon(
self.metadata_entry = None ImageEnum.EDITDELETE, ICON_SIZE
self.filename_combobox = None )
self.dir_list = None self.notebook: Optional[ttk.Notebook] = None
self.startup_commands_listbox = None self.metadata_entry: Optional[ttk.Entry] = None
self.shutdown_commands_listbox = None self.filename_combobox: Optional[ttk.Combobox] = None
self.validate_commands_listbox = None self.dir_list: Optional[ListboxScroll] = None
self.validation_time_entry = None self.startup_commands_listbox: Optional[tk.Listbox] = None
self.validation_mode_entry = None self.shutdown_commands_listbox: Optional[tk.Listbox] = None
self.service_file_data = None self.validate_commands_listbox: Optional[tk.Listbox] = None
self.validation_period_entry = None self.validation_time_entry: Optional[ttk.Entry] = None
self.original_service_files = {} self.validation_mode_entry: Optional[ttk.Entry] = None
self.default_config = None self.service_file_data: Optional[CodeText] = None
self.temp_service_files = {} self.validation_period_entry: Optional[ttk.Entry] = None
self.modified_files = set() self.original_service_files: Dict[str, str] = {}
self.has_error = False self.default_config: NodeServiceData = None
self.temp_service_files: Dict[str, str] = {}
self.modified_files: Set[str] = set()
self.has_error: bool = False
self.load() self.load()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def load(self): def load(self) -> None:
try: try:
self.app.core.create_nodes_and_links() self.app.core.create_nodes_and_links()
default_config = self.app.core.get_node_service( default_config = self.app.core.get_node_service(
@ -119,7 +124,7 @@ class ServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Get Node Service Error", e) self.app.show_grpc_exception("Get Node Service Error", e)
self.has_error = True self.has_error = True
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)
@ -142,7 +147,7 @@ class ServiceConfigDialog(Dialog):
self.draw_buttons() self.draw_buttons()
def draw_tab_files(self): def draw_tab_files(self) -> None:
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)
@ -222,7 +227,7 @@ class ServiceConfigDialog(Dialog):
"<FocusOut>", self.update_temp_service_file_data "<FocusOut>", self.update_temp_service_file_data
) )
def draw_tab_directories(self): def draw_tab_directories(self) -> None:
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)
@ -257,7 +262,7 @@ class ServiceConfigDialog(Dialog):
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")
def draw_tab_startstop(self): def draw_tab_startstop(self) -> None:
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)
@ -311,7 +316,7 @@ class ServiceConfigDialog(Dialog):
elif i == 2: elif i == 2:
self.validate_commands_listbox = listbox_scroll.listbox self.validate_commands_listbox = listbox_scroll.listbox
def draw_tab_configuration(self): def draw_tab_configuration(self) -> None:
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)
@ -370,7 +375,7 @@ class ServiceConfigDialog(Dialog):
for dependency in self.dependencies: for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency) listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(4): for i in range(4):
@ -384,7 +389,7 @@ class ServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy) 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")
def add_filename(self): def add_filename(self) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
if filename not in self.filename_combobox["values"]: if filename not in self.filename_combobox["values"]:
self.filename_combobox["values"] += (filename,) self.filename_combobox["values"] += (filename,)
@ -395,7 +400,7 @@ class ServiceConfigDialog(Dialog):
else: else:
logging.debug("file already existed") logging.debug("file already existed")
def delete_filename(self): def delete_filename(self) -> None:
cbb = self.filename_combobox cbb = self.filename_combobox
filename = cbb.get() filename = cbb.get()
if filename in cbb["values"]: if filename in cbb["values"]:
@ -407,7 +412,7 @@ class ServiceConfigDialog(Dialog):
self.modified_files.remove(filename) self.modified_files.remove(filename)
@classmethod @classmethod
def add_command(cls, event: tk.Event): def add_command(cls, event: tk.Event) -> None:
frame_contains_button = event.widget.master frame_contains_button = event.widget.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
@ -419,7 +424,7 @@ class ServiceConfigDialog(Dialog):
listbox.insert(tk.END, command_to_add) listbox.insert(tk.END, command_to_add)
@classmethod @classmethod
def update_entry(cls, event: tk.Event): def update_entry(cls, event: tk.Event) -> None:
listbox = event.widget listbox = event.widget
current_selection = listbox.curselection() current_selection = listbox.curselection()
if len(current_selection) > 0: if len(current_selection) > 0:
@ -431,7 +436,7 @@ class ServiceConfigDialog(Dialog):
entry.insert(0, cmd) entry.insert(0, cmd)
@classmethod @classmethod
def delete_command(cls, event: tk.Event): def delete_command(cls, event: tk.Event) -> None:
button = event.widget button = event.widget
frame_contains_button = button.master frame_contains_button = button.master
listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
@ -441,7 +446,7 @@ class ServiceConfigDialog(Dialog):
entry = frame_contains_button.grid_slaves(row=0, column=0)[0] entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
entry.delete(0, tk.END) entry.delete(0, tk.END)
def click_apply(self): def click_apply(self) -> None:
if ( if (
not self.is_custom_command() not self.is_custom_command()
and not self.is_custom_service_file() and not self.is_custom_service_file()
@ -484,12 +489,12 @@ class ServiceConfigDialog(Dialog):
self.app.show_grpc_exception("Save Service Config Error", e) 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) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
self.service_file_data.text.delete(1.0, "end") self.service_file_data.text.delete(1.0, "end")
self.service_file_data.text.insert("end", self.temp_service_files[filename]) self.service_file_data.text.insert("end", self.temp_service_files[filename])
def update_temp_service_file_data(self, event: tk.Event): def update_temp_service_file_data(self, event: tk.Event) -> None:
filename = self.filename_combobox.get() filename = self.filename_combobox.get()
self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end") self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
if self.temp_service_files[filename] != self.original_service_files.get( if self.temp_service_files[filename] != self.original_service_files.get(
@ -499,7 +504,7 @@ class ServiceConfigDialog(Dialog):
else: else:
self.modified_files.discard(filename) self.modified_files.discard(filename)
def is_custom_command(self): def is_custom_command(self) -> bool:
startup, validate, shutdown = self.get_commands() startup, validate, shutdown = self.get_commands()
return ( return (
set(self.default_startup) != set(startup) set(self.default_startup) != set(startup)
@ -507,16 +512,16 @@ class ServiceConfigDialog(Dialog):
or set(self.default_shutdown) != set(shutdown) or set(self.default_shutdown) != set(shutdown)
) )
def has_new_files(self): def has_new_files(self) -> bool:
return set(self.filenames) != set(self.filename_combobox["values"]) return set(self.filenames) != set(self.filename_combobox["values"])
def is_custom_service_file(self): def is_custom_service_file(self) -> bool:
return len(self.modified_files) > 0 return len(self.modified_files) > 0
def is_custom_directory(self): def is_custom_directory(self) -> bool:
return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end")) return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
def click_defaults(self): def click_defaults(self) -> None:
""" """
clears out any custom configuration permanently clears out any custom configuration permanently
""" """
@ -557,37 +562,41 @@ class ServiceConfigDialog(Dialog):
self.current_service_color("") self.current_service_color("")
def click_copy(self): def click_copy(self) -> None:
dialog = CopyServiceConfigDialog(self, self.app, self.node_id) file_name = self.filename_combobox.get()
name = self.canvas_node.core_node.name
dialog = CopyServiceConfigDialog(
self.app, self, name, self.service_name, file_name
)
dialog.show() dialog.show()
@classmethod @classmethod
def append_commands( def append_commands(
cls, commands: List[str], listbox: tk.Listbox, to_add: List[str] cls, commands: List[str], listbox: tk.Listbox, to_add: List[str]
): ) -> None:
for cmd in to_add: for cmd in to_add:
commands.append(cmd) commands.append(cmd)
listbox.insert(tk.END, cmd) listbox.insert(tk.END, cmd)
def get_commands(self): def get_commands(self) -> Tuple[List[str], List[str], List[str]]:
startup = self.startup_commands_listbox.get(0, "end") startup = self.startup_commands_listbox.get(0, "end")
shutdown = self.shutdown_commands_listbox.get(0, "end") shutdown = self.shutdown_commands_listbox.get(0, "end")
validate = self.validate_commands_listbox.get(0, "end") validate = self.validate_commands_listbox.get(0, "end")
return startup, validate, shutdown return startup, validate, shutdown
def find_directory_button(self): def find_directory_button(self) -> None:
d = filedialog.askdirectory(initialdir="/") d = filedialog.askdirectory(initialdir="/")
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def add_directory(self): def add_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()
if os.path.isdir(d): if os.path.isdir(d):
if d not in self.temp_directories: if d not in self.temp_directories:
self.dir_list.listbox.insert("end", d) self.dir_list.listbox.insert("end", d)
self.temp_directories.append(d) self.temp_directories.append(d)
def remove_directory(self): def remove_directory(self) -> None:
d = self.directory_entry.get() d = self.directory_entry.get()
dirs = self.dir_list.listbox.get(0, "end") dirs = self.dir_list.listbox.get(0, "end")
if d and d in self.temp_directories: if d and d in self.temp_directories:
@ -599,14 +608,14 @@ class ServiceConfigDialog(Dialog):
logging.debug("directory is not in the list") logging.debug("directory is not in the list")
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
def directory_select(self, event): def directory_select(self, event) -> None:
i = self.dir_list.listbox.curselection() i = self.dir_list.listbox.curselection()
if i: if i:
d = self.dir_list.listbox.get(i) d = self.dir_list.listbox.get(i)
self.directory_entry.delete(0, "end") self.directory_entry.delete(0, "end")
self.directory_entry.insert("end", d) self.directory_entry.insert("end", d)
def current_service_color(self, color=""): def current_service_color(self, color="") -> None:
""" """
change the current service label color change the current service label color
""" """

View file

@ -1,9 +1,11 @@
import logging import logging
import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
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 ConfigFrame from core.gui.widgets import ConfigFrame
@ -13,15 +15,16 @@ if TYPE_CHECKING:
class SessionOptionsDialog(Dialog): class SessionOptionsDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Session Options") super().__init__(app, "Session Options")
self.config_frame = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.has_error: bool = False
self.config = self.get_config() self.config: Dict[str, ConfigOption] = self.get_config()
self.enabled: bool = not self.app.core.is_runtime()
if not self.has_error: if not self.has_error:
self.draw() self.draw()
def get_config(self): def get_config(self) -> Dict[str, ConfigOption]:
try: try:
session_id = self.app.core.session_id session_id = self.app.core.session_id
response = self.app.core.client.get_session_options(session_id) response = self.app.core.client.get_session_options(session_id)
@ -31,11 +34,10 @@ class SessionOptionsDialog(Dialog):
self.has_error = True 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(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
self.config_frame = ConfigFrame(self.top, self.app, config=self.config)
self.config_frame.draw_config() self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY) self.config_frame.grid(sticky="nsew", pady=PADY)
@ -43,12 +45,13 @@ class SessionOptionsDialog(Dialog):
frame.grid(sticky="ew") frame.grid(sticky="ew")
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.save) state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Save", command=self.save, state=state)
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 save(self): def save(self) -> None:
config = self.config_frame.parse_config() config = self.config_frame.parse_config()
try: try:
session_id = self.app.core.session_id session_id = self.app.core.session_id

View file

@ -1,11 +1,12 @@
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, List from typing import TYPE_CHECKING, List, Optional
import grpc import grpc
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import SessionSummary
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.task import ProgressTask from core.gui.task import ProgressTask
@ -18,17 +19,17 @@ if TYPE_CHECKING:
class SessionsDialog(Dialog): class SessionsDialog(Dialog):
def __init__(self, app: "Application", is_start_app: bool = False) -> None: def __init__(self, app: "Application", is_start_app: bool = False) -> None:
super().__init__(app, "Sessions") super().__init__(app, "Sessions")
self.is_start_app = is_start_app self.is_start_app: bool = is_start_app
self.selected_session = None self.selected_session: Optional[int] = None
self.selected_id = None self.selected_id: Optional[int] = None
self.tree = None self.tree: Optional[ttk.Treeview] = None
self.sessions = self.get_sessions() self.sessions: List[SessionSummary] = self.get_sessions()
self.connect_button = None self.connect_button: Optional[ttk.Button] = None
self.delete_button = None self.delete_button: Optional[ttk.Button] = None
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.draw() self.draw()
def get_sessions(self) -> List[core_pb2.SessionSummary]: def get_sessions(self) -> List[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)

View file

@ -3,7 +3,7 @@ shape input dialog
""" """
import tkinter as tk import tkinter as tk
from tkinter import font, ttk from tkinter import font, ttk
from typing import TYPE_CHECKING, List, Union from typing import TYPE_CHECKING, List, Optional, Union
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
@ -13,40 +13,41 @@ from core.gui.themes import FRAME_PAD, 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
from core.gui.graph.shape import Shape from core.gui.graph.shape import Shape
FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] FONT_SIZES: List[int] = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]
BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] BORDER_WIDTH: List[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class ShapeDialog(Dialog): class ShapeDialog(Dialog):
def __init__(self, app: "Application", shape: "Shape"): def __init__(self, app: "Application", shape: "Shape") -> None:
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__(app, title) super().__init__(app, title)
self.canvas = app.canvas self.canvas: "CanvasGraph" = app.canvas
self.fill = None self.fill: Optional[ttk.Label] = None
self.border = None self.border: Optional[ttk.Label] = None
self.shape = shape self.shape: "Shape" = shape
data = shape.shape_data data = shape.shape_data
self.shape_text = tk.StringVar(value=data.text) self.shape_text: tk.StringVar = tk.StringVar(value=data.text)
self.font = tk.StringVar(value=data.font) self.font: tk.StringVar = tk.StringVar(value=data.font)
self.font_size = tk.IntVar(value=data.font_size) self.font_size: tk.IntVar = tk.IntVar(value=data.font_size)
self.text_color = data.text_color self.text_color: str = data.text_color
fill_color = data.fill_color fill_color = data.fill_color
if not fill_color: if not fill_color:
fill_color = "#CFCFFF" fill_color = "#CFCFFF"
self.fill_color = fill_color self.fill_color: str = fill_color
self.border_color = data.border_color self.border_color: str = data.border_color
self.border_width = tk.IntVar(value=0) self.border_width: tk.IntVar = tk.IntVar(value=0)
self.bold = tk.BooleanVar(value=data.bold) self.bold: tk.BooleanVar = tk.BooleanVar(value=data.bold)
self.italic = tk.BooleanVar(value=data.italic) self.italic: tk.BooleanVar = tk.BooleanVar(value=data.italic)
self.underline = tk.BooleanVar(value=data.underline) self.underline: tk.BooleanVar = tk.BooleanVar(value=data.underline)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw_label_options() self.draw_label_options()
if is_draw_shape(self.shape.shape_type): if is_draw_shape(self.shape.shape_type):
@ -54,7 +55,7 @@ class ShapeDialog(Dialog):
self.draw_spacer() self.draw_spacer()
self.draw_buttons() self.draw_buttons()
def draw_label_options(self): def draw_label_options(self) -> None:
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
label_frame.grid(sticky="ew") label_frame.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -94,7 +95,7 @@ class ShapeDialog(Dialog):
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
button.grid(row=0, column=2, sticky="ew") button.grid(row=0, column=2, sticky="ew")
def draw_shape_options(self): def draw_shape_options(self) -> None:
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
label_frame.grid(sticky="ew", pady=PADY) label_frame.grid(sticky="ew", pady=PADY)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
@ -129,7 +130,7 @@ class ShapeDialog(Dialog):
) )
combobox.grid(row=0, column=1, sticky="nsew") combobox.grid(row=0, column=1, sticky="nsew")
def draw_buttons(self): def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="nsew") frame.grid(sticky="nsew")
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
@ -139,28 +140,28 @@ class ShapeDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.cancel) button = ttk.Button(frame, text="Cancel", command=self.cancel)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky="ew")
def choose_text_color(self): def choose_text_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.text_color) color_picker = ColorPickerDialog(self, self.app, self.text_color)
self.text_color = color_picker.askcolor() self.text_color = color_picker.askcolor()
def choose_fill_color(self): def choose_fill_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.fill_color) color_picker = ColorPickerDialog(self, self.app, self.fill_color)
color = color_picker.askcolor() color = color_picker.askcolor()
self.fill_color = color self.fill_color = color
self.fill.config(background=color, text=color) self.fill.config(background=color, text=color)
def choose_border_color(self): def choose_border_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.border_color) color_picker = ColorPickerDialog(self, self.app, self.border_color)
color = color_picker.askcolor() color = color_picker.askcolor()
self.border_color = color self.border_color = color
self.border.config(background=color, text=color) self.border.config(background=color, text=color)
def cancel(self): def cancel(self) -> None:
self.shape.delete() self.shape.delete()
self.canvas.shapes.pop(self.shape.id) self.canvas.shapes.pop(self.shape.id)
self.destroy() self.destroy()
def click_add(self): def click_add(self) -> None:
if is_draw_shape(self.shape.shape_type): if is_draw_shape(self.shape.shape_type):
self.add_shape() self.add_shape()
elif is_shape_text(self.shape.shape_type): elif is_shape_text(self.shape.shape_type):
@ -181,7 +182,7 @@ class ShapeDialog(Dialog):
text_font.append("underline") text_font.append("underline")
return text_font return text_font
def save_text(self): def save_text(self) -> None:
""" """
save info related to text or shape label save info related to text or shape label
""" """
@ -194,7 +195,7 @@ class ShapeDialog(Dialog):
data.italic = self.italic.get() data.italic = self.italic.get()
data.underline = self.underline.get() data.underline = self.underline.get()
def save_shape(self): def save_shape(self) -> None:
""" """
save info related to shape save info related to shape
""" """
@ -203,7 +204,7 @@ class ShapeDialog(Dialog):
data.border_color = self.border_color data.border_color = self.border_color
data.border_width = int(self.border_width.get()) data.border_width = int(self.border_width.get())
def add_text(self): def add_text(self) -> None:
""" """
add text to canvas add text to canvas
""" """
@ -214,7 +215,7 @@ class ShapeDialog(Dialog):
) )
self.save_text() self.save_text()
def add_shape(self): def add_shape(self) -> None:
self.canvas.itemconfig( self.canvas.itemconfig(
self.shape.id, self.shape.id,
fill=self.fill_color, fill=self.fill_color,

View file

@ -3,10 +3,11 @@ throughput dialog
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING: if TYPE_CHECKING:
@ -14,21 +15,23 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog): class ThroughputDialog(Dialog):
def __init__(self, app: "Application"): def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config") super().__init__(app, "Throughput Config")
self.canvas = app.canvas self.canvas: CanvasGraph = app.canvas
self.show_throughput = tk.IntVar(value=1) self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight = tk.IntVar(value=1) self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission = tk.IntVar(value=1) self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception = tk.IntVar(value=1) self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold) self.threshold: tk.DoubleVar = tk.DoubleVar(
self.width = tk.IntVar(value=self.canvas.throughput_width) value=self.canvas.throughput_threshold
self.color = self.canvas.throughput_color )
self.color_button = None self.width: tk.IntVar = tk.IntVar(value=self.canvas.throughput_width)
self.color: str = self.canvas.throughput_color
self.color_button: Optional[tk.Button] = None
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.draw() self.draw()
def draw(self): def draw(self) -> None:
button = ttk.Checkbutton( button = ttk.Checkbutton(
self.top, self.top,
variable=self.show_throughput, variable=self.show_throughput,
@ -97,12 +100,12 @@ class ThroughputDialog(Dialog):
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_color(self): def click_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.color) color_picker = ColorPickerDialog(self, self.app, self.color)
self.color = color_picker.askcolor() self.color = color_picker.askcolor()
self.color_button.config(bg=self.color, text=self.color, bd=0) self.color_button.config(bg=self.color, text=self.color, bd=0)
def click_save(self): def click_save(self) -> None:
self.canvas.throughput_threshold = self.threshold.get() self.canvas.throughput_threshold = self.threshold.get()
self.canvas.throughput_width = self.width.get() self.canvas.throughput_width = self.width.get()
self.canvas.throughput_color = self.color self.canvas.throughput_color = self.color

View file

@ -1,8 +1,10 @@
from tkinter import ttk from tkinter import ttk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
import grpc import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import Node
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 ConfigFrame from core.gui.widgets import ConfigFrame
@ -10,34 +12,36 @@ from core.gui.widgets import ConfigFrame
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 from core.gui.graph.node import CanvasNode
from core.gui.graph.graph import CanvasGraph
RANGE_COLOR = "#009933" RANGE_COLOR: str = "#009933"
RANGE_WIDTH = 3 RANGE_WIDTH: int = 3
class WlanConfigDialog(Dialog): class WlanConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode"): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration") super().__init__(app, f"{canvas_node.core_node.name} WLAN Configuration")
self.canvas_node = canvas_node self.canvas: "CanvasGraph" = app.canvas
self.node = canvas_node.core_node self.canvas_node: "CanvasNode" = canvas_node
self.config_frame = None self.node: Node = canvas_node.core_node
self.range_entry = None self.config_frame: Optional[ConfigFrame] = None
self.has_error = False self.range_entry: Optional[ttk.Entry] = None
self.canvas = app.canvas self.has_error: bool = False
self.ranges = {} self.ranges: Dict[int, int] = {}
self.positive_int = self.app.master.register(self.validate_and_update) self.positive_int: int = self.app.master.register(self.validate_and_update)
try: try:
self.config = self.canvas_node.wlan_config config = self.canvas_node.wlan_config
if not self.config: if not config:
self.config = self.app.core.get_wlan_config(self.node.id) config = self.app.core.get_wlan_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.init_draw_range() self.init_draw_range()
self.draw() self.draw()
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("WLAN Config Error", e) self.app.show_grpc_exception("WLAN Config Error", e)
self.has_error = True self.has_error: bool = True
self.destroy() self.destroy()
def init_draw_range(self): def init_draw_range(self) -> None:
if self.canvas_node.id in self.canvas.wireless_network: if self.canvas_node.id in self.canvas.wireless_network:
for cid in self.canvas.wireless_network[self.canvas_node.id]: for cid in self.canvas.wireless_network[self.canvas_node.id]:
x, y = self.canvas.coords(cid) x, y = self.canvas.coords(cid)
@ -46,7 +50,7 @@ class WlanConfigDialog(Dialog):
) )
self.ranges[cid] = range_id self.ranges[cid] = range_id
def draw(self): def draw(self) -> None:
self.top.columnconfigure(0, weight=1) self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame = ConfigFrame(self.top, self.app, self.config)
@ -55,7 +59,7 @@ class WlanConfigDialog(Dialog):
self.draw_apply_buttons() self.draw_apply_buttons()
self.top.bind("<Destroy>", self.remove_ranges) self.top.bind("<Destroy>", self.remove_ranges)
def draw_apply_buttons(self): def draw_apply_buttons(self) -> None:
""" """
create node configuration options create node configuration options
""" """
@ -75,7 +79,7 @@ class WlanConfigDialog(Dialog):
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_apply(self): def click_apply(self) -> None:
""" """
retrieve user's wlan configuration and store the new configuration values retrieve user's wlan configuration and store the new configuration values
""" """
@ -87,7 +91,7 @@ class WlanConfigDialog(Dialog):
self.remove_ranges() self.remove_ranges()
self.destroy() self.destroy()
def remove_ranges(self, event=None): def remove_ranges(self, event=None) -> None:
for cid in self.canvas.find_withtag("range"): for cid in self.canvas.find_withtag("range"):
self.canvas.delete(cid) self.canvas.delete(cid)
self.ranges.clear() self.ranges.clear()

View file

View file

@ -0,0 +1,36 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
class InfoFrameBase(ttk.Frame):
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(master, padding=FRAME_PAD)
self.app: "Application" = app
def draw(self) -> None:
raise NotImplementedError
class DetailsFrame(ttk.Frame):
def __init__(self, master: tk.BaseWidget) -> None:
super().__init__(master)
self.columnconfigure(1, weight=1)
self.row = 0
def add_detail(self, label: str, value: str) -> None:
label = ttk.Label(self, text=label, anchor=tk.W)
label.grid(row=self.row, sticky=tk.EW, column=0, padx=PADX)
label = ttk.Label(self, text=value, anchor=tk.W, state=tk.DISABLED)
label.grid(row=self.row, sticky=tk.EW, column=1)
self.row += 1
def add_separator(self) -> None:
separator = ttk.Separator(self)
separator.grid(row=self.row, sticky=tk.EW, columnspan=2, pady=PADY)
self.row += 1

View file

@ -0,0 +1,19 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from core.gui.frames.base import InfoFrameBase
if TYPE_CHECKING:
from core.gui.app import Application
class DefaultInfoFrame(InfoFrameBase):
def __init__(self, master: tk.BaseWidget, app: "Application") -> None:
super().__init__(master, app)
def draw(self) -> None:
label = ttk.Label(self, text="Click a Node/Link", anchor=tk.CENTER)
label.grid(sticky=tk.EW)
label = ttk.Label(self, text="to see details", anchor=tk.CENTER)
label.grid(sticky=tk.EW)

View file

@ -0,0 +1,113 @@
import tkinter as tk
from typing import TYPE_CHECKING, Optional
from core.api.grpc.core_pb2 import Interface
from core.gui.frames.base import DetailsFrame, InfoFrameBase
from core.gui.utils import bandwidth_text
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.edges import CanvasEdge
from core.gui.graph.node import CanvasNode
from core.gui.graph.edges import CanvasWirelessEdge
def get_iface(canvas_node: "CanvasNode", net_id: int) -> Optional[Interface]:
iface = None
for edge in canvas_node.edges:
link = edge.link
if link.node1_id == net_id:
iface = link.iface2
elif link.node2_id == net_id:
iface = link.iface1
return iface
class EdgeInfoFrame(InfoFrameBase):
def __init__(
self, master: tk.BaseWidget, app: "Application", edge: "CanvasEdge"
) -> None:
super().__init__(master, app)
self.edge: "CanvasEdge" = edge
def draw(self) -> None:
self.columnconfigure(0, weight=1)
link = self.edge.link
options = link.options
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
dst_node = dst_canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.add_detail("Source", src_node.name)
iface1 = link.iface1
if iface1:
mac = iface1.mac if iface1.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else ""
frame.add_detail("IP6", ip6)
frame.add_separator()
frame.add_detail("Destination", dst_node.name)
iface2 = link.iface2
if iface2:
mac = iface2.mac if iface2.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else ""
frame.add_detail("IP6", ip6)
if link.HasField("options"):
frame.add_separator()
bandwidth = bandwidth_text(options.bandwidth)
frame.add_detail("Bandwidth", bandwidth)
frame.add_detail("Delay", f"{options.delay} us")
frame.add_detail("Jitter", f"\u00B1{options.jitter} us")
frame.add_detail("Loss", f"{options.loss}%")
frame.add_detail("Duplicate", f"{options.dup}%")
class WirelessEdgeInfoFrame(InfoFrameBase):
def __init__(
self, master: tk.BaseWidget, app: "Application", edge: "CanvasWirelessEdge"
) -> None:
super().__init__(master, app)
self.edge: "CanvasWirelessEdge" = edge
def draw(self) -> None:
link = self.edge.link
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
dst_node = dst_canvas_node.core_node
# find interface for each node connected to network
net_id = link.network_id
iface1 = get_iface(src_canvas_node, net_id)
iface2 = get_iface(dst_canvas_node, net_id)
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.add_detail("Source", src_node.name)
if iface1:
mac = iface1.mac if iface1.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface1.ip4}/{iface1.ip4_mask}" if iface1.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface1.ip6}/{iface1.ip6_mask}" if iface1.ip6 else ""
frame.add_detail("IP6", ip6)
frame.add_separator()
frame.add_detail("Destination", dst_node.name)
if iface2:
mac = iface2.mac if iface2.mac else "auto"
frame.add_detail("MAC", mac)
ip4 = f"{iface2.ip4}/{iface2.ip4_mask}" if iface2.ip4 else ""
frame.add_detail("IP4", ip4)
ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else ""
frame.add_detail("IP6", ip6)

View file

@ -0,0 +1,39 @@
from typing import TYPE_CHECKING
from core.api.grpc.core_pb2 import NodeType
from core.gui.frames.base import DetailsFrame, InfoFrameBase
from core.gui.nodeutils import NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeInfoFrame(InfoFrameBase):
def __init__(self, master, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(master, app)
self.canvas_node: "CanvasNode" = canvas_node
def draw(self) -> None:
self.columnconfigure(0, weight=1)
node = self.canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.add_detail("ID", node.id)
frame.add_detail("Name", node.name)
if NodeUtils.is_model_node(node.type):
frame.add_detail("Type", node.model)
if NodeUtils.is_container_node(node.type):
for index, service in enumerate(sorted(node.services)):
if index == 0:
frame.add_detail("Services", service)
else:
frame.add_detail("", service)
if node.type == NodeType.EMANE:
emane = node.emane.split("_")[1:]
frame.add_detail("EMANE", emane)
if NodeUtils.is_image_node(node.type):
frame.add_detail("Image", node.image)
if NodeUtils.is_container_node(node.type):
server = node.server if node.server else "localhost"
frame.add_detail("Server", server)

View file

@ -1,23 +1,26 @@
import logging import logging
import math import math
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING, Any, Tuple from typing import TYPE_CHECKING, Optional, Tuple
from core.api.grpc import core_pb2 from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import Interface, Link
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
TEXT_DISTANCE = 0.30 TEXT_DISTANCE: float = 0.30
EDGE_WIDTH = 3 EDGE_WIDTH: int = 3
EDGE_COLOR = "#ff0000" EDGE_COLOR: str = "#ff0000"
WIRELESS_WIDTH = 1.5 WIRELESS_WIDTH: float = 3
WIRELESS_COLOR = "#009933" WIRELESS_COLOR: str = "#009933"
ARC_DISTANCE = 50 ARC_DISTANCE: int = 50
def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]: def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
@ -57,20 +60,20 @@ def arc_edges(edges) -> None:
class Edge: class Edge:
tag = tags.EDGE tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas self.canvas = canvas
self.id = None self.id: Optional[int] = None
self.src = src self.src: int = src
self.dst = dst self.dst: int = dst
self.arc = 0 self.arc: int = 0
self.token = None self.token: Optional[Tuple[int, ...]] = None
self.src_label = None self.src_label: Optional[int] = None
self.middle_label = None self.middle_label: Optional[int] = None
self.dst_label = None self.dst_label: Optional[int] = None
self.color = EDGE_COLOR self.color: str = EDGE_COLOR
self.width = EDGE_WIDTH self.width: int = EDGE_WIDTH
@classmethod @classmethod
def create_token(cls, src: int, dst: int) -> Tuple[int, ...]: def create_token(cls, src: int, dst: int) -> Tuple[int, ...]:
@ -120,7 +123,7 @@ class Edge:
fill=self.color, fill=self.color,
) )
def redraw(self): def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color) self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id) src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y src_pos = src_x, src_y
@ -139,11 +142,16 @@ class Edge:
font=self.canvas.app.edge_font, font=self.canvas.app.edge_font,
text=text, text=text,
tags=tags.LINK_LABEL, tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.canvas.show_link_labels.state(), state=self.canvas.show_link_labels.state(),
) )
else: else:
self.canvas.itemconfig(self.middle_label, text=text) self.canvas.itemconfig(self.middle_label, text=text)
def clear_middle_label(self) -> None:
self.canvas.delete(self.middle_label)
self.middle_label = None
def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: 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) src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v1 = dst_x - src_x v1 = dst_x - src_x
@ -215,11 +223,10 @@ class Edge:
logging.debug("deleting canvas edge, id: %s", self.id) 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.src_label)
self.canvas.delete(self.middle_label)
self.canvas.delete(self.dst_label) self.canvas.delete(self.dst_label)
self.clear_middle_label()
self.id = None self.id = None
self.src_label = None self.src_label = None
self.middle_label = None
self.dst_label = None self.dst_label = None
@ -233,14 +240,28 @@ class CanvasWirelessEdge(Edge):
dst: int, dst: int,
src_pos: Tuple[float, float], src_pos: Tuple[float, float],
dst_pos: Tuple[float, float], dst_pos: Tuple[float, float],
token: Tuple[Any, ...], token: Tuple[int, ...],
link: Link,
) -> None: ) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst) logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst) super().__init__(canvas, src, dst)
self.token = token self.link: Link = link
self.width = WIRELESS_WIDTH self.token: Tuple[int, ...] = token
self.color = WIRELESS_COLOR self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR
self.color: str = color
self.draw(src_pos, dst_pos) self.draw(src_pos, dst_pos)
if link.label:
self.middle_label_text(link.label)
self.set_binding()
def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(
WirelessEdgeInfoFrame, app=self.canvas.app, edge=self
)
class CanvasEdge(Edge): class CanvasEdge(Edge):
@ -259,55 +280,57 @@ class CanvasEdge(Edge):
Create an instance of canvas edge object Create an instance of canvas edge object
""" """
super().__init__(canvas, src) super().__init__(canvas, src)
self.src_interface = None self.src_iface: Optional[Interface] = None
self.dst_interface = None self.dst_iface: Optional[Interface] = None
self.text_src = None self.text_src: Optional[int] = None
self.text_dst = None self.text_dst: Optional[int] = None
self.link = None self.link: Optional[Link] = None
self.asymmetric_link = None self.asymmetric_link: Optional[Link] = None
self.throughput = None self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos) self.draw(src_pos, dst_pos)
self.set_binding() self.set_binding()
self.context = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context() self.create_context()
def create_context(self): def create_context(self) -> None:
themes.style_menu(self.context) themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure) self.context.add_command(label="Configure", command=self.click_configure)
self.context.add_command(label="Delete", command=self.click_delete) self.context.add_command(label="Delete", command=self.click_delete)
def set_binding(self) -> None: def set_binding(self) -> None:
self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context) self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def set_link(self, link) -> None: def set_link(self, link: Link) -> None:
self.link = link self.link = link
self.draw_labels() self.draw_labels()
def interface_label(self, interface: core_pb2.Interface) -> str: def iface_label(self, iface: core_pb2.Interface) -> str:
label = "" label = ""
if interface.name and self.canvas.show_interface_names.get(): if iface.name and self.canvas.show_iface_names.get():
label = f"{interface.name}" label = f"{iface.name}"
if interface.ip4 and self.canvas.show_ip4s.get(): if iface.ip4 and self.canvas.show_ip4s.get():
label = f"{label}\n" if label else "" label = f"{label}\n" if label else ""
label += f"{interface.ip4}/{interface.ip4mask}" label += f"{iface.ip4}/{iface.ip4_mask}"
if interface.ip6 and self.canvas.show_ip6s.get(): if iface.ip6 and self.canvas.show_ip6s.get():
label = f"{label}\n" if label else "" label = f"{label}\n" if label else ""
label += f"{interface.ip6}/{interface.ip6mask}" label += f"{iface.ip6}/{iface.ip6_mask}"
return label return label
def create_node_labels(self) -> Tuple[str, str]: def create_node_labels(self) -> Tuple[str, str]:
label_one = None label1 = None
if self.link.HasField("interface_one"): if self.link.HasField("iface1"):
label_one = self.interface_label(self.link.interface_one) label1 = self.iface_label(self.link.iface1)
label_two = None label2 = None
if self.link.HasField("interface_two"): if self.link.HasField("iface2"):
label_two = self.interface_label(self.link.interface_two) label2 = self.iface_label(self.link.iface2)
return label_one, label_two return label1, label2
def draw_labels(self) -> None: def draw_labels(self) -> None:
src_text, dst_text = self.create_node_labels() src_text, dst_text = self.create_node_labels()
self.src_label_text(src_text) self.src_label_text(src_text)
self.dst_label_text(dst_text) self.dst_label_text(dst_text)
self.draw_link_options()
def redraw(self) -> None: def redraw(self) -> None:
super().redraw() super().redraw()
@ -378,14 +401,38 @@ class CanvasEdge(Edge):
self.middle_label = None self.middle_label = None
self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width()) self.canvas.itemconfig(self.id, fill=self.color, width=self.scaled_width())
def show_info(self, _event: tk.Event) -> None:
self.canvas.app.display_info(EdgeInfoFrame, app=self.canvas.app, edge=self)
def show_context(self, event: tk.Event) -> None: def show_context(self, event: tk.Event) -> None:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state) self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root) self.context.tk_popup(event.x_root, event.y_root)
def click_delete(self): def click_delete(self) -> None:
self.canvas.delete_edge(self) self.canvas.delete_edge(self)
def click_configure(self) -> None: def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self) dialog = LinkConfigurationDialog(self.canvas.app, self)
dialog.show() dialog.show()
def draw_link_options(self):
options = self.link.options
lines = []
bandwidth = options.bandwidth
if bandwidth > 0:
lines.append(bandwidth_text(bandwidth))
delay = options.delay
jitter = options.jitter
if delay > 0 and jitter > 0:
lines.append(f"{delay} us (\u00B1{jitter} us)")
elif jitter > 0:
lines.append(f"0 us (\u00B1{jitter} us)")
loss = options.loss
if loss > 0:
lines.append(f"loss={loss}%")
dup = options.dup
if dup > 0:
lines.append(f"dup={dup}%")
label = "\n".join(lines)
self.middle_label_text(label)

View file

@ -2,11 +2,19 @@ import logging
import tkinter as tk import tkinter as tk
from copy import deepcopy from copy import deepcopy
from tkinter import BooleanVar from tkinter import BooleanVar
from typing import TYPE_CHECKING, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from PIL import Image, ImageTk from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import (
Interface,
Link,
LinkType,
Node,
Session,
ThroughputsEvent,
)
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import ( from core.gui.graph.edges import (
@ -21,7 +29,7 @@ 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, TypeToImage from core.gui.images import ImageEnum, TypeToImage
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeDraw, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -48,58 +56,59 @@ class ShowVar(BooleanVar):
class CanvasGraph(tk.Canvas): class CanvasGraph(tk.Canvas):
def __init__(self, master: tk.Widget, app: "Application", core: "CoreClient"): def __init__(
self, master: tk.BaseWidget, app: "Application", core: "CoreClient"
) -> None:
super().__init__(master, highlightthickness=0, background="#cccccc") super().__init__(master, highlightthickness=0, background="#cccccc")
self.app = app self.app: "Application" = app
self.core = core self.core: "CoreClient" = core
self.mode = GraphMode.SELECT self.mode: GraphMode = GraphMode.SELECT
self.annotation_type = None self.annotation_type: Optional[ShapeType] = None
self.selection = {} self.selection: Dict[int, int] = {}
self.select_box = None self.select_box: Optional[Shape] = None
self.selected = None self.selected: Optional[int] = None
self.node_draw = None self.node_draw: Optional[NodeDraw] = None
self.nodes = {} self.nodes: Dict[int, CanvasNode] = {}
self.edges = {} self.edges: Dict[int, CanvasEdge] = {}
self.shapes = {} self.shapes: Dict[int, Shape] = {}
self.wireless_edges = {} self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {}
# map wireless/EMANE node to the set of MDRs connected to that node # map wireless/EMANE node to the set of MDRs connected to that node
self.wireless_network = {} self.wireless_network: Dict[int, Set[int]] = {}
self.drawing_edge = None self.drawing_edge: Optional[CanvasEdge] = None
self.rect = None self.rect: Optional[int] = None
self.shape_drawing = False self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height height = self.app.guiconfig.preferences.height
self.default_dimensions = (width, height) self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions = self.default_dimensions self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.ratio = 1.0 self.ratio: float = 1.0
self.offset = (0, 0) self.offset: Tuple[int, int] = (0, 0)
self.cursor = (0, 0) self.cursor: Tuple[int, int] = (0, 0)
self.marker_tool = None self.to_copy: List[CanvasNode] = []
self.to_copy = []
# background related # background related
self.wallpaper_id = None self.wallpaper_id: Optional[int] = None
self.wallpaper = None self.wallpaper: Optional[Image.Image] = None
self.wallpaper_drawn = None self.wallpaper_drawn: Optional[PhotoImage] = None
self.wallpaper_file = "" self.wallpaper_file: str = ""
self.scale_option = tk.IntVar(value=1) self.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim = tk.BooleanVar(value=False) self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)
# throughput related # throughput related
self.throughput_threshold = 250.0 self.throughput_threshold: float = 250.0
self.throughput_width = 10 self.throughput_width: int = 10
self.throughput_color = "#FF0000" self.throughput_color: str = "#FF0000"
# drawing related # drawing related
self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True) self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True) self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_grid = ShowVar(self, tags.GRIDLINE, value=True) self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True) self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_interface_names = BooleanVar(value=False) self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s = BooleanVar(value=True) self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s = BooleanVar(value=True) self.show_ip6s: BooleanVar = BooleanVar(value=True)
# bindings # bindings
self.setup_bindings() self.setup_bindings()
@ -108,7 +117,7 @@ class CanvasGraph(tk.Canvas):
self.draw_canvas() self.draw_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) -> None:
if self.rect is not None: if self.rect is not None:
self.delete(self.rect) self.delete(self.rect)
if not dimensions: if not dimensions:
@ -125,7 +134,7 @@ class CanvasGraph(tk.Canvas):
) )
self.configure(scrollregion=self.bbox(tk.ALL)) self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: core_pb2.Session): def reset_and_redraw(self, session: Session) -> None:
""" """
Reset the private variables CanvasGraph object, redraw nodes given the new grpc Reset the private variables CanvasGraph object, redraw nodes given the new grpc
client. client.
@ -136,7 +145,7 @@ class CanvasGraph(tk.Canvas):
self.show_link_labels.set(True) self.show_link_labels.set(True)
self.show_grid.set(True) self.show_grid.set(True)
self.show_annotations.set(True) self.show_annotations.set(True)
self.show_interface_names.set(False) self.show_iface_names.set(False)
self.show_ip4s.set(True) self.show_ip4s.set(True)
self.show_ip6s.set(True) self.show_ip6s.set(True)
@ -157,7 +166,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge = None self.drawing_edge = None
self.draw_session(session) self.draw_session(session)
def setup_bindings(self): def setup_bindings(self) -> None:
""" """
Bind any mouse events or hot keys to the matching action Bind any mouse events or hot keys to the matching action
""" """
@ -173,43 +182,43 @@ class CanvasGraph(tk.Canvas):
self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y)) self.bind("<ButtonPress-3>", lambda e: self.scan_mark(e.x, e.y))
self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1)) self.bind("<B3-Motion>", lambda e: self.scan_dragto(e.x, e.y, gain=1))
def get_actual_coords(self, x: float, y: float) -> [float, float]: def get_actual_coords(self, x: float, y: float) -> Tuple[float, float]:
actual_x = (x - self.offset[0]) / self.ratio actual_x = (x - self.offset[0]) / self.ratio
actual_y = (y - self.offset[1]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio
return actual_x, actual_y return actual_x, actual_y
def get_scaled_coords(self, x: float, y: float) -> [float, float]: def get_scaled_coords(self, x: float, y: float) -> Tuple[float, float]:
scaled_x = (x * self.ratio) + self.offset[0] scaled_x = (x * self.ratio) + self.offset[0]
scaled_y = (y * self.ratio) + self.offset[1] scaled_y = (y * self.ratio) + self.offset[1]
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) -> Tuple[bool, bool]:
x1, y1, x2, y2 = self.bbox(self.rect) 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
def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> [bool, bool]: def valid_position(self, x1: int, y1: int, x2: int, y2: int) -> Tuple[bool, bool]:
valid_topleft = self.inside_canvas(x1, y1) valid_topleft = self.inside_canvas(x1, y1)
valid_bottomright = self.inside_canvas(x2, y2) valid_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright return valid_topleft and valid_bottomright
def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent): def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None:
for interface_throughput in throughputs_event.interface_throughputs: for iface_throughput in throughputs_event.iface_throughputs:
node_id = interface_throughput.node_id node_id = iface_throughput.node_id
interface_id = interface_throughput.interface_id iface_id = iface_throughput.iface_id
throughput = interface_throughput.throughput throughput = iface_throughput.throughput
interface_to_edge_id = (node_id, interface_id) iface_to_edge_id = (node_id, iface_id)
token = self.core.interface_to_edge.get(interface_to_edge_id) token = self.core.iface_to_edge.get(iface_to_edge_id)
if not token: if not token:
continue continue
edge = self.edges.get(token) edge = self.edges.get(token)
if edge: if edge:
edge.set_throughput(throughput) edge.set_throughput(throughput)
else: else:
del self.core.interface_to_edge[interface_to_edge_id] del self.core.iface_to_edge[iface_to_edge_id]
def draw_grid(self): def draw_grid(self) -> None:
""" """
Create grid. Create grid.
""" """
@ -223,9 +232,51 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE) self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect) self.tag_lower(self.rect)
def add_wireless_edge( def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link token = create_edge_token(src.id, dst.id)
) -> None: if token in self.edges and link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
elif token not in self.edges:
node1 = src.core_node
node2 = dst.core_node
src_pos = (node1.position.x, node1.position.y)
dst_pos = (node2.position.x, node2.position.y)
edge = CanvasEdge(self, src.id, src_pos, dst_pos)
edge.token = token
edge.dst = dst.id
edge.set_link(link)
edge.check_wireless()
src.edges.add(edge)
dst.edges.add(edge)
self.edges[edge.token] = edge
self.core.links[edge.token] = edge
if link.HasField("iface1"):
iface1 = link.iface1
self.core.iface_to_edge[(node1.id, iface1.id)] = token
src.ifaces[iface1.id] = iface1
edge.src_iface = iface1
if link.HasField("iface2"):
iface2 = link.iface2
self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
dst.ifaces[iface2.id] = iface2
edge.dst_iface = iface2
def delete_wired_edge(self, src: CanvasNode, dst: CanvasNode) -> None:
token = create_edge_token(src.id, dst.id)
edge = self.edges.get(token)
if not edge:
return
self.delete_edge(edge)
def update_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
token = create_edge_token(src.id, dst.id)
edge = self.edges.get(token)
if not edge:
return
edge.link.options.CopyFrom(link.options)
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
network_id = link.network_id if link.network_id else None network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id) token = create_edge_token(src.id, dst.id, network_id)
if token in self.wireless_edges: if token in self.wireless_edges:
@ -233,11 +284,7 @@ class CanvasGraph(tk.Canvas):
return return
src_pos = self.coords(src.id) src_pos = self.coords(src.id)
dst_pos = self.coords(dst.id) dst_pos = self.coords(dst.id)
edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token) edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token, link)
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)
@ -248,7 +295,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges) arc_edges(common_edges)
def delete_wireless_edge( def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None: ) -> None:
network_id = link.network_id if link.network_id else None network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id) token = create_edge_token(src.id, dst.id, network_id)
@ -263,7 +310,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges) arc_edges(common_edges)
def update_wireless_edge( def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None: ) -> None:
if not link.label: if not link.label:
return return
@ -275,20 +322,13 @@ class CanvasGraph(tk.Canvas):
edge = self.wireless_edges[token] edge = self.wireless_edges[token]
edge.middle_label_text(link.label) edge.middle_label_text(link.label)
def draw_session(self, session: core_pb2.Session): def add_core_node(self, core_node: Node) -> None:
""" if core_node.id in self.core.canvas_nodes:
Draw existing session. logging.error("core node already exists: %s", core_node)
""" return
# draw existing nodes logging.debug("adding node %s", core_node)
for core_node in session.nodes:
logging.debug("drawing node %s", core_node)
# peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type):
continue
image = NodeUtils.node_image(
core_node, self.app.guiconfig, self.app.app_scale
)
# if the gui can't find node's image, default to the "edit-node" image # if the gui can't find node's image, default to the "edit-node" image
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image: if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE) image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
x = core_node.position.x x = core_node.position.x
@ -297,51 +337,28 @@ class CanvasGraph(tk.Canvas):
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
def draw_session(self, session: Session) -> None:
"""
Draw existing session.
"""
# draw existing nodes
for core_node in session.nodes:
# peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type):
continue
self.add_core_node(core_node)
# draw existing links # draw existing links
for link in session.links: for link in session.links:
logging.debug("drawing link: %s", link) logging.debug("drawing link: %s", link)
canvas_node_one = self.core.canvas_nodes[link.node_one_id] canvas_node1 = self.core.canvas_nodes[link.node1_id]
node_one = canvas_node_one.core_node canvas_node2 = self.core.canvas_nodes[link.node2_id]
canvas_node_two = self.core.canvas_nodes[link.node_two_id] if link.type == LinkType.WIRELESS:
node_two = canvas_node_two.core_node self.add_wireless_edge(canvas_node1, canvas_node2, link)
token = create_edge_token(canvas_node_one.id, canvas_node_two.id)
if link.type == core_pb2.LinkType.WIRELESS:
self.add_wireless_edge(canvas_node_one, canvas_node_two, link)
else: else:
if token not in self.edges: self.add_wired_edge(canvas_node1, canvas_node2, link)
src_pos = (node_one.position.x, node_one.position.y)
dst_pos = (node_two.position.x, node_two.position.y)
edge = CanvasEdge(self, canvas_node_one.id, src_pos, dst_pos)
edge.token = token
edge.dst = canvas_node_two.id
edge.set_link(link)
edge.check_wireless()
canvas_node_one.edges.add(edge)
canvas_node_two.edges.add(edge)
self.edges[edge.token] = edge
self.core.links[edge.token] = edge
if link.HasField("interface_one"):
interface_one = link.interface_one
self.core.interface_to_edge[
(node_one.id, interface_one.id)
] = token
canvas_node_one.interfaces[interface_one.id] = interface_one
edge.src_interface = interface_one
if link.HasField("interface_two"):
interface_two = link.interface_two
self.core.interface_to_edge[
(node_two.id, interface_two.id)
] = edge.token
canvas_node_two.interfaces[interface_two.id] = interface_two
edge.dst_interface = interface_two
elif link.options.unidirectional:
edge = self.edges[token]
edge.asymmetric_link = link
else:
logging.error("duplicate link received: %s", link)
def stopped_session(self): def stopped_session(self) -> None:
# clear wireless edges # clear wireless edges
for edge in self.wireless_edges.values(): for edge in self.wireless_edges.values():
edge.delete() edge.delete()
@ -351,11 +368,10 @@ class CanvasGraph(tk.Canvas):
dst_node.wireless_edges.remove(edge) dst_node.wireless_edges.remove(edge)
self.wireless_edges.clear() self.wireless_edges.clear()
# clear all middle edge labels # clear throughputs
for edge in self.edges.values(): self.clear_throughputs()
edge.reset()
def canvas_xy(self, event: tk.Event) -> [float, float]: def canvas_xy(self, event: tk.Event) -> Tuple[float, float]:
""" """
Convert window coordinate to canvas coordinate Convert window coordinate to canvas coordinate
""" """
@ -383,7 +399,7 @@ class CanvasGraph(tk.Canvas):
return selected return selected
def click_release(self, event: tk.Event): def click_release(self, event: tk.Event) -> None:
""" """
Draw a node or finish drawing an edge according to the current graph mode Draw a node or finish drawing an edge according to the current graph mode
""" """
@ -422,7 +438,7 @@ class CanvasGraph(tk.Canvas):
self.mode = GraphMode.NODE 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) -> None:
edge = self.drawing_edge edge = self.drawing_edge
self.drawing_edge = None self.drawing_edge = None
@ -432,8 +448,9 @@ class CanvasGraph(tk.Canvas):
# edge dst must be a node # edge dst must be a node
logging.debug("current selected: %s", self.selected) logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected) dst_node = self.nodes.get(self.selected)
if not dst_node: if not dst_node or not src_node:
edge.delete() edge.delete()
return return
@ -448,17 +465,23 @@ class CanvasGraph(tk.Canvas):
edge.delete() edge.delete()
return return
# rj45 nodes can only support one link
if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges:
edge.delete()
return
if NodeUtils.is_rj45_node(dst_node.core_node.type) and dst_node.edges:
edge.delete()
return
# set dst node and snap edge to center # set dst node and snap edge to center
edge.complete(self.selected) edge.complete(self.selected)
self.edges[edge.token] = edge self.edges[edge.token] = edge
node_src = self.nodes[edge.src] src_node.edges.add(edge)
node_src.edges.add(edge) dst_node.edges.add(edge)
node_dst = self.nodes[edge.dst] self.core.create_link(edge, src_node, dst_node)
node_dst.edges.add(edge)
self.core.create_link(edge, node_src, node_dst)
def select_object(self, object_id: int, choose_multiple: bool = False): def select_object(self, object_id: int, choose_multiple: bool = False) -> None:
""" """
create a bounding box when a node is selected create a bounding box when a node is selected
""" """
@ -479,7 +502,7 @@ class CanvasGraph(tk.Canvas):
selection_id = self.selection.pop(object_id) selection_id = self.selection.pop(object_id)
self.delete(selection_id) self.delete(selection_id)
def clear_selection(self): def clear_selection(self) -> None:
""" """
Clear current selection boxes. Clear current selection boxes.
""" """
@ -487,7 +510,7 @@ class CanvasGraph(tk.Canvas):
self.delete(_id) self.delete(_id)
self.selection.clear() self.selection.clear()
def move_selection(self, object_id: int, x_offset: float, y_offset: float): def move_selection(self, object_id: int, x_offset: float, y_offset: float) -> None:
select_id = self.selection.get(object_id) select_id = self.selection.get(object_id)
if select_id is not None: if select_id is not None:
self.move(select_id, x_offset, y_offset) self.move(select_id, x_offset, y_offset)
@ -515,14 +538,14 @@ class CanvasGraph(tk.Canvas):
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_iface = edge.src_iface
if edge.src == object_id: if edge.src == object_id:
other_id = edge.dst other_id = edge.dst
other_interface = edge.dst_interface other_iface = edge.dst_iface
other_node = self.nodes[other_id] other_node = self.nodes[other_id]
other_node.edges.remove(edge) other_node.edges.remove(edge)
if other_interface: if other_iface:
del other_node.interfaces[other_interface.id] del other_node.ifaces[other_iface.id]
if is_wireless: if is_wireless:
other_node.delete_antenna() other_node.delete_antenna()
@ -535,17 +558,17 @@ class CanvasGraph(tk.Canvas):
self.core.deleted_graph_nodes(nodes) self.core.deleted_graph_nodes(nodes)
self.core.deleted_graph_edges(edges) self.core.deleted_graph_edges(edges)
def delete_edge(self, edge: CanvasEdge): def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete() edge.delete()
del self.edges[edge.token] del self.edges[edge.token]
src_node = self.nodes[edge.src] src_node = self.nodes[edge.src]
src_node.edges.discard(edge) src_node.edges.discard(edge)
if edge.src_interface: if edge.src_iface:
del src_node.interfaces[edge.src_interface.id] del src_node.ifaces[edge.src_iface.id]
dst_node = self.nodes[edge.dst] dst_node = self.nodes[edge.dst]
dst_node.edges.discard(edge) dst_node.edges.discard(edge)
if edge.dst_interface: if edge.dst_iface:
del dst_node.interfaces[edge.dst_interface.id] del dst_node.ifaces[edge.dst_iface.id]
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
if src_wireless: if src_wireless:
dst_node.delete_antenna() dst_node.delete_antenna()
@ -554,7 +577,7 @@ class CanvasGraph(tk.Canvas):
src_node.delete_antenna() src_node.delete_antenna()
self.core.deleted_graph_edges([edge]) self.core.deleted_graph_edges([edge])
def zoom(self, event: tk.Event, factor: float = None): def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor: if not factor:
factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
@ -572,7 +595,7 @@ class CanvasGraph(tk.Canvas):
if self.wallpaper: if self.wallpaper:
self.redraw_wallpaper() self.redraw_wallpaper()
def click_press(self, event: tk.Event): def click_press(self, event: tk.Event) -> None:
""" """
Start drawing an edge if mouse click is on a node Start drawing an edge if mouse click is on a node
""" """
@ -634,7 +657,7 @@ class CanvasGraph(tk.Canvas):
self.select_box = shape self.select_box = shape
self.clear_selection() self.clear_selection()
def ctrl_click(self, event: tk.Event): def ctrl_click(self, event: tk.Event) -> None:
# update cursor location # update cursor location
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):
@ -652,7 +675,7 @@ class CanvasGraph(tk.Canvas):
): ):
self.select_object(selected, choose_multiple=True) self.select_object(selected, choose_multiple=True)
def click_motion(self, event: tk.Event): def click_motion(self, event: tk.Event) -> None:
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):
if self.select_box: if self.select_box:
@ -705,17 +728,18 @@ 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 press_delete(self, _event: tk.Event): def press_delete(self, _event: tk.Event) -> None:
""" """
delete selected nodes and any data that relates to it delete selected nodes and any data that relates to it
""" """
logging.debug("press delete key") logging.debug("press delete key")
if not self.app.core.is_runtime(): if not self.app.core.is_runtime():
self.delete_selected_objects() self.delete_selected_objects()
self.app.default_info()
else: else:
logging.debug("node deletion is disabled during runtime state") logging.debug("node deletion is disabled during runtime state")
def double_click(self, event: tk.Event): def double_click(self, event: tk.Event) -> None:
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]
@ -741,7 +765,7 @@ class CanvasGraph(tk.Canvas):
self.core.canvas_nodes[core_node.id] = node self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node self.nodes[node.id] = node
def width_and_height(self): def width_and_height(self) -> Tuple[int, int]:
""" """
retrieve canvas width and height in pixels retrieve canvas width and height in pixels
""" """
@ -757,8 +781,8 @@ class CanvasGraph(tk.Canvas):
return image return image
def draw_wallpaper( def draw_wallpaper(
self, image: ImageTk.PhotoImage, x: float = None, y: float = None self, image: PhotoImage, x: float = None, y: float = None
): ) -> None:
if x is None and y is None: if x is None and y is None:
x1, y1, x2, y2 = self.bbox(self.rect) x1, y1, x2, y2 = self.bbox(self.rect)
x = (x1 + x2) / 2 x = (x1 + x2) / 2
@ -766,7 +790,7 @@ class CanvasGraph(tk.Canvas):
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)
self.wallpaper_drawn = image self.wallpaper_drawn = image
def wallpaper_upper_left(self): def wallpaper_upper_left(self) -> None:
self.delete(self.wallpaper_id) self.delete(self.wallpaper_id)
# create new scaled image, cropped if needed # create new scaled image, cropped if needed
@ -779,7 +803,7 @@ class CanvasGraph(tk.Canvas):
if image.height > height: if image.height > height:
cropy = image.height cropy = image.height
cropped = image.crop((0, 0, cropx, cropy)) cropped = image.crop((0, 0, cropx, cropy))
image = ImageTk.PhotoImage(cropped) image = PhotoImage(cropped)
# draw on canvas # draw on canvas
x1, y1, _, _ = self.bbox(self.rect) x1, y1, _, _ = self.bbox(self.rect)
@ -787,7 +811,7 @@ class CanvasGraph(tk.Canvas):
y = (cropy / 2) + y1 y = (cropy / 2) + y1
self.draw_wallpaper(image, x, y) self.draw_wallpaper(image, x, y)
def wallpaper_center(self): def wallpaper_center(self) -> None:
""" """
place the image at the center of canvas place the image at the center of canvas
""" """
@ -807,26 +831,26 @@ class CanvasGraph(tk.Canvas):
x2 = image.width - cropx x2 = image.width - cropx
y2 = image.height - cropy y2 = image.height - cropy
cropped = image.crop((x1, y1, x2, y2)) cropped = image.crop((x1, y1, x2, y2))
image = ImageTk.PhotoImage(cropped) image = PhotoImage(cropped)
self.draw_wallpaper(image) self.draw_wallpaper(image)
def wallpaper_scaled(self): def wallpaper_scaled(self) -> None:
""" """
scale image based on canvas dimension scale image based on canvas dimension
""" """
self.delete(self.wallpaper_id) self.delete(self.wallpaper_id)
canvas_w, canvas_h = self.width_and_height() canvas_w, canvas_h = self.width_and_height()
image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS)
image = ImageTk.PhotoImage(image) image = PhotoImage(image)
self.draw_wallpaper(image) self.draw_wallpaper(image)
def resize_to_wallpaper(self): def resize_to_wallpaper(self) -> None:
self.delete(self.wallpaper_id) self.delete(self.wallpaper_id)
image = ImageTk.PhotoImage(self.wallpaper) image = PhotoImage(self.wallpaper)
self.redraw_canvas((image.width(), image.height())) self.redraw_canvas((image.width(), image.height()))
self.draw_wallpaper(image) self.draw_wallpaper(image)
def redraw_canvas(self, dimensions: Tuple[int, int] = None): def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None:
logging.debug("redrawing canvas to dimensions: %s", dimensions) logging.debug("redrawing canvas to dimensions: %s", dimensions)
# reset scale and move back to original position # reset scale and move back to original position
@ -847,7 +871,7 @@ class CanvasGraph(tk.Canvas):
self.draw_grid() self.draw_grid()
self.app.canvas.show_grid.click_handler() self.app.canvas.show_grid.click_handler()
def redraw_wallpaper(self): def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get(): if self.adjust_to_dim.get():
logging.debug("drawing wallpaper to canvas dimensions") logging.debug("drawing wallpaper to canvas dimensions")
self.resize_to_wallpaper() self.resize_to_wallpaper()
@ -868,7 +892,7 @@ class CanvasGraph(tk.Canvas):
for tag in tags.ORGANIZE_TAGS: for tag in tags.ORGANIZE_TAGS:
self.tag_raise(tag) self.tag_raise(tag)
def set_wallpaper(self, filename: str): def set_wallpaper(self, filename: Optional[str]) -> None:
logging.debug("setting wallpaper: %s", filename) logging.debug("setting wallpaper: %s", filename)
if filename: if filename:
img = Image.open(filename) img = Image.open(filename)
@ -884,7 +908,7 @@ class CanvasGraph(tk.Canvas):
def is_selection_mode(self) -> bool: def is_selection_mode(self) -> bool:
return self.mode == GraphMode.SELECT return self.mode == GraphMode.SELECT
def create_edge(self, source: CanvasNode, dest: CanvasNode): def create_edge(self, source: CanvasNode, dest: CanvasNode) -> None:
""" """
create an edge between source node and destination node create an edge between source node and destination node
""" """
@ -898,7 +922,7 @@ class CanvasGraph(tk.Canvas):
self.nodes[dest.id].edges.add(edge) self.nodes[dest.id].edges.add(edge)
self.core.create_link(edge, source, dest) self.core.create_link(edge, source, dest)
def copy(self): def copy(self) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("copy is disabled during runtime state") logging.debug("copy is disabled during runtime state")
return return
@ -909,7 +933,7 @@ class CanvasGraph(tk.Canvas):
canvas_node = self.nodes[node_id] canvas_node = self.nodes[node_id]
self.to_copy.append(canvas_node) self.to_copy.append(canvas_node)
def paste(self): def paste(self) -> None:
if self.core.is_runtime(): if self.core.is_runtime():
logging.debug("paste is disabled during runtime state") logging.debug("paste is disabled during runtime state")
return return
@ -965,26 +989,26 @@ class CanvasGraph(tk.Canvas):
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)
interface_one = None iface1_id = None
if copy_link.HasField("interface_one"): if copy_link.HasField("iface1"):
interface_one = copy_link.interface_one.id iface1_id = copy_link.iface1.id
interface_two = None iface2_id = None
if copy_link.HasField("interface_two"): if copy_link.HasField("iface2"):
interface_two = copy_link.interface_two.id iface2_id = copy_link.iface2.id
if not options.unidirectional: if not options.unidirectional:
copy_edge.asymmetric_link = None copy_edge.asymmetric_link = None
else: else:
asym_interface_one = None asym_iface1 = None
if interface_one: if iface1_id:
asym_interface_one = core_pb2.Interface(id=interface_one) asym_iface1 = Interface(id=iface1_id)
asym_interface_two = None asym_iface2 = None
if interface_two: if iface2_id:
asym_interface_two = core_pb2.Interface(id=interface_two) asym_iface2 = Interface(id=iface2_id)
copy_edge.asymmetric_link = core_pb2.Link( copy_edge.asymmetric_link = Link(
node_one_id=copy_link.node_two_id, node1_id=copy_link.node2_id,
node_two_id=copy_link.node_one_id, node2_id=copy_link.node1_id,
interface_one=asym_interface_one, iface1=asym_iface1,
interface_two=asym_interface_two, iface2=asym_iface2,
options=edge.asymmetric_link.options, options=edge.asymmetric_link.options,
) )
self.itemconfig( self.itemconfig(
@ -994,7 +1018,12 @@ class CanvasGraph(tk.Canvas):
) )
self.tag_raise(tags.NODE) self.tag_raise(tags.NODE)
def scale_graph(self): def clear_throughputs(self) -> None:
for edge in self.edges.values():
edge.clear_middle_label()
edge.draw_link_options()
def scale_graph(self) -> None:
for nid, canvas_node in self.nodes.items(): for nid, canvas_node in self.nodes.items():
img = None img = None
if NodeUtils.is_custom( if NodeUtils.is_custom(

View file

@ -1,12 +1,14 @@
import functools import functools
import logging import logging
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
import grpc import grpc
from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2 from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import NodeType from core.api.grpc.core_pb2 import Interface, Node, NodeType
from core.api.grpc.services_pb2 import NodeServiceData
from core.gui import themes from core.gui import themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
@ -14,37 +16,33 @@ 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.frames.node import NodeInfoFrame
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, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum from core.gui.images import ImageEnum
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from PIL.ImageTk import PhotoImage from core.gui.graph.graph import CanvasGraph
NODE_TEXT_OFFSET = 5 NODE_TEXT_OFFSET: int = 5
class CanvasNode: class CanvasNode:
def __init__( def __init__(
self, self, app: "Application", x: float, y: float, core_node: Node, image: PhotoImage
app: "Application",
x: float,
y: float,
core_node: core_pb2.Node,
image: "PhotoImage",
): ):
self.app = app self.app: "Application" = app
self.canvas = app.canvas self.canvas: "CanvasGraph" = app.canvas
self.image = image self.image: PhotoImage = image
self.core_node = core_node self.core_node: Node = core_node
self.id = self.canvas.create_image( self.id: int = self.canvas.create_image(
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
) )
label_y = self._get_label_y() label_y = self._get_label_y()
self.text_id = self.canvas.create_text( self.text_id: int = self.canvas.create_text(
x, x,
label_y, label_y,
text=self.core_node.name, text=self.core_node.name,
@ -53,42 +51,45 @@ class CanvasNode:
fill="#0000CD", fill="#0000CD",
state=self.canvas.show_node_labels.state(), state=self.canvas.show_node_labels.state(),
) )
self.tooltip = CanvasTooltip(self.canvas) self.tooltip: CanvasTooltip = CanvasTooltip(self.canvas)
self.edges = set() self.edges: Set[CanvasEdge] = set()
self.interfaces = {} self.ifaces: Dict[int, Interface] = {}
self.wireless_edges = set() self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas = [] self.antennas: List[int] = []
self.antenna_images = {} self.antenna_images: Dict[int, PhotoImage] = {}
# possible configurations # possible configurations
self.emane_model_configs = {} self.emane_model_configs: Dict[
self.wlan_config = {} Tuple[str, Optional[int]], Dict[str, ConfigOption]
self.mobility_config = {} ] = {}
self.service_configs = {} self.wlan_config: Dict[str, ConfigOption] = {}
self.service_file_configs = {} self.mobility_config: Dict[str, ConfigOption] = {}
self.config_service_configs = {} self.service_configs: Dict[str, NodeServiceData] = {}
self.service_file_configs: Dict[str, Dict[str, str]] = {}
self.config_service_configs: Dict[str, Any] = {}
self.setup_bindings() self.setup_bindings()
self.context = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context) themes.style_menu(self.context)
def next_interface_id(self) -> int: def next_iface_id(self) -> int:
i = 0 i = 0
while i in self.interfaces: while i in self.ifaces:
i += 1 i += 1
return i return i
def setup_bindings(self): def setup_bindings(self) -> None:
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) self.canvas.tag_bind(self.id, "<ButtonRelease-3>", self.show_context)
self.canvas.tag_bind(self.id, "<Button-1>", self.show_info)
def delete(self): def delete(self) -> None:
logging.debug("Delete canvas node for %s", self.core_node) logging.debug("Delete canvas node for %s", self.core_node)
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)
self.delete_antennas() self.delete_antennas()
def add_antenna(self): def add_antenna(self) -> None:
x, y = self.canvas.coords(self.id) x, y = self.canvas.coords(self.id)
offset = len(self.antennas) * 8 * self.app.app_scale offset = len(self.antennas) * 8 * self.app.app_scale
img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) img = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)
@ -102,7 +103,7 @@ class CanvasNode:
self.antennas.append(antenna_id) self.antennas.append(antenna_id)
self.antenna_images[antenna_id] = img self.antenna_images[antenna_id] = img
def delete_antenna(self): def delete_antenna(self) -> None:
""" """
delete one antenna delete one antenna
""" """
@ -112,7 +113,7 @@ class CanvasNode:
self.canvas.delete(antenna_id) self.canvas.delete(antenna_id)
self.antenna_images.pop(antenna_id, None) self.antenna_images.pop(antenna_id, None)
def delete_antennas(self): def delete_antennas(self) -> None:
""" """
delete all antennas delete all antennas
""" """
@ -122,30 +123,30 @@ class CanvasNode:
self.antennas.clear() self.antennas.clear()
self.antenna_images.clear() self.antenna_images.clear()
def redraw(self): def redraw(self) -> None:
self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.id, image=self.image)
self.canvas.itemconfig(self.text_id, text=self.core_node.name) self.canvas.itemconfig(self.text_id, text=self.core_node.name)
for edge in self.edges: for edge in self.edges:
edge.redraw() edge.redraw()
def _get_label_y(self): def _get_label_y(self) -> int:
image_box = self.canvas.bbox(self.id) image_box = self.canvas.bbox(self.id)
return image_box[3] + NODE_TEXT_OFFSET return image_box[3] + NODE_TEXT_OFFSET
def scale_text(self): def scale_text(self) -> None:
text_bound = self.canvas.bbox(self.text_id) text_bound = self.canvas.bbox(self.text_id)
prev_y = (text_bound[3] + text_bound[1]) / 2 prev_y = (text_bound[3] + text_bound[1]) / 2
new_y = self._get_label_y() new_y = self._get_label_y()
self.canvas.move(self.text_id, 0, new_y - prev_y) self.canvas.move(self.text_id, 0, new_y - prev_y)
def move(self, x: int, y: int): def move(self, x: float, y: float) -> None:
x, y = self.canvas.get_scaled_coords(x, y) x, y = self.canvas.get_scaled_coords(x, y)
current_x, current_y = self.canvas.coords(self.id) current_x, current_y = self.canvas.coords(self.id)
x_offset = x - current_x x_offset = x - current_x
y_offset = y - current_y y_offset = y - current_y
self.motion(x_offset, y_offset, update=False) self.motion(x_offset, y_offset, update=False)
def motion(self, x_offset: int, y_offset: int, update: bool = True): def motion(self, x_offset: float, y_offset: float, update: bool = True) -> None:
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)
pos = self.canvas.coords(self.id) pos = self.canvas.coords(self.id)
@ -177,8 +178,11 @@ class CanvasNode:
if self.app.core.is_runtime() and update: if self.app.core.is_runtime() and update:
self.app.core.edit_node(self.core_node) self.app.core.edit_node(self.core_node)
def on_enter(self, event: tk.Event): def on_enter(self, event: tk.Event) -> None:
if self.app.core.is_runtime() and self.app.core.observer: is_runtime = self.app.core.is_runtime()
has_observer = self.app.core.observer is not None
is_container = NodeUtils.is_container_node(self.core_node.type)
if is_runtime and has_observer and is_container:
self.tooltip.text.set("waiting...") self.tooltip.text.set("waiting...")
self.tooltip.on_enter(event) self.tooltip.on_enter(event)
try: try:
@ -187,15 +191,19 @@ class CanvasNode:
except grpc.RpcError as e: except grpc.RpcError as e:
self.app.show_grpc_exception("Observer Error", e) self.app.show_grpc_exception("Observer Error", e)
def on_leave(self, event: tk.Event): def on_leave(self, event: tk.Event) -> None:
self.tooltip.on_leave(event) self.tooltip.on_leave(event)
def double_click(self, event: tk.Event): def double_click(self, event: tk.Event) -> None:
if self.app.core.is_runtime(): if self.app.core.is_runtime():
if NodeUtils.is_container_node(self.core_node.type):
self.canvas.core.launch_terminal(self.core_node.id) self.canvas.core.launch_terminal(self.core_node.id)
else: else:
self.show_config() self.show_config()
def show_info(self, _event: tk.Event) -> None:
self.app.display_info(NodeInfoFrame, app=self.app, canvas_node=self)
def show_context(self, event: tk.Event) -> None: def show_context(self, event: tk.Event) -> None:
# clear existing menu # clear existing menu
self.context.delete(0, tk.END) self.context.delete(0, tk.END)
@ -203,6 +211,10 @@ class CanvasNode:
is_emane = self.core_node.type == NodeType.EMANE is_emane = self.core_node.type == NodeType.EMANE
if self.app.core.is_runtime(): if self.app.core.is_runtime():
self.context.add_command(label="Configure", command=self.show_config) self.context.add_command(label="Configure", command=self.show_config)
if is_emane:
self.context.add_command(
label="EMANE Config", command=self.show_emane_config
)
if is_wlan: if is_wlan:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config label="WLAN Config", command=self.show_wlan_config
@ -259,57 +271,58 @@ class CanvasNode:
def click_unlink(self, edge: CanvasEdge) -> None: def click_unlink(self, edge: CanvasEdge) -> None:
self.canvas.delete_edge(edge) self.canvas.delete_edge(edge)
self.app.default_info()
def canvas_delete(self) -> None: def canvas_delete(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()
self.canvas.selection[self.id] = self self.canvas.select_object(self.id)
self.canvas.delete_selected_objects() self.canvas.delete_selected_objects()
def canvas_copy(self) -> None: def canvas_copy(self) -> None:
self.canvas.clear_selection() self.canvas.clear_selection()
self.canvas.selection[self.id] = self self.canvas.select_object(self.id)
self.canvas.copy() self.canvas.copy()
def show_config(self): def show_config(self) -> None:
dialog = NodeConfigDialog(self.app, self) dialog = NodeConfigDialog(self.app, self)
dialog.show() dialog.show()
def show_wlan_config(self): def show_wlan_config(self) -> None:
dialog = WlanConfigDialog(self.app, self) dialog = WlanConfigDialog(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) -> None:
dialog = MobilityConfigDialog(self.app, self) dialog = MobilityConfigDialog(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) -> 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) -> None:
dialog = EmaneConfigDialog(self.app, self) dialog = EmaneConfigDialog(self.app, self)
dialog.show() dialog.show()
def show_services(self): def show_services(self) -> None:
dialog = NodeServiceDialog(self.app, self) dialog = NodeServiceDialog(self.app, self)
dialog.show() dialog.show()
def show_config_services(self): def show_config_services(self) -> None:
dialog = NodeConfigServiceDialog(self.app, self) dialog = NodeConfigServiceDialog(self.app, self)
dialog.show() dialog.show()
def has_emane_link(self, interface_id: int) -> core_pb2.Node: def has_emane_link(self, iface_id: int) -> Node:
result = None result = None
for edge in self.edges: for edge in self.edges:
if self.id == edge.src: if self.id == edge.src:
other_id = edge.dst other_id = edge.dst
edge_interface_id = edge.src_interface.id edge_iface_id = edge.src_iface.id
else: else:
other_id = edge.src other_id = edge.src
edge_interface_id = edge.dst_interface.id edge_iface_id = edge.dst_iface.id
if edge_interface_id != interface_id: if edge_iface_id != iface_id:
continue continue
other_node = self.canvas.nodes[other_id] other_node = self.canvas.nodes[other_id]
if other_node.core_node.type == NodeType.EMANE: if other_node.core_node.type == NodeType.EMANE:
@ -317,14 +330,14 @@ class CanvasNode:
break break
return result return result
def wireless_link_selected(self): def wireless_link_selected(self) -> None:
nodes = [x for x in self.canvas.selection if x in self.canvas.nodes] nodes = [x for x in self.canvas.selection if x in self.canvas.nodes]
for node_id in nodes: for node_id in nodes:
canvas_node = self.canvas.nodes[node_id] canvas_node = self.canvas.nodes[node_id]
self.canvas.create_edge(self, canvas_node) self.canvas.create_edge(self, canvas_node)
self.canvas.clear_selection() self.canvas.clear_selection()
def scale_antennas(self): def scale_antennas(self) -> None:
for i in range(len(self.antennas)): for i in range(len(self.antennas)):
antenna_id = self.antennas[i] antenna_id = self.antennas[i]
image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE) image = self.app.get_icon(ImageEnum.ANTENNA, ANTENNA_SIZE)

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Union from typing import TYPE_CHECKING, Dict, List, Optional, Union
from core.gui.dialogs.shapemod import ShapeDialog from core.gui.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
@ -23,17 +23,17 @@ class AnnotationData:
bold: bool = False, bold: bool = False,
italic: bool = False, italic: bool = False,
underline: bool = False, underline: bool = False,
): ) -> None:
self.text = text self.text: str = text
self.font = font self.font: str = font
self.font_size = font_size self.font_size: int = font_size
self.text_color = text_color self.text_color: str = text_color
self.fill_color = fill_color self.fill_color: str = fill_color
self.border_color = border_color self.border_color: str = border_color
self.border_width = border_width self.border_width: int = border_width
self.bold = bold self.bold: bool = bold
self.italic = italic self.italic: bool = italic
self.underline = underline self.underline: bool = underline
class Shape: class Shape:
@ -47,29 +47,29 @@ class Shape:
x2: float = None, x2: float = None,
y2: float = None, y2: float = None,
data: AnnotationData = None, data: AnnotationData = None,
): ) -> None:
self.app = app self.app: "Application" = app
self.canvas = canvas self.canvas: "CanvasGraph" = canvas
self.shape_type = shape_type self.shape_type: ShapeType = shape_type
self.id = None self.id: Optional[int] = None
self.text_id = None self.text_id: Optional[int] = None
self.x1 = x1 self.x1: float = x1
self.y1 = y1 self.y1: float = y1
if x2 is None: if x2 is None:
x2 = x1 x2 = x1
self.x2 = x2 self.x2: float = x2
if y2 is None: if y2 is None:
y2 = y1 y2 = y1
self.y2 = y2 self.y2: float = y2
if data is None: if data is None:
self.created = False self.created: bool = False
self.shape_data = AnnotationData() self.shape_data: AnnotationData = AnnotationData()
else: else:
self.created = True self.created: bool = True
self.shape_data = data self.shape_data = data
self.draw() self.draw()
def draw(self): def draw(self) -> None:
if self.created: if self.created:
dash = None dash = None
else: else:
@ -127,7 +127,7 @@ class Shape:
font.append("underline") font.append("underline")
return font return font
def draw_shape_text(self): def draw_shape_text(self) -> None:
if self.shape_data.text: if self.shape_data.text:
x = (self.x1 + self.x2) / 2 x = (self.x1 + self.x2) / 2
y = self.y1 + 1.5 * self.shape_data.font_size y = self.y1 + 1.5 * self.shape_data.font_size
@ -142,18 +142,18 @@ class Shape:
state=self.canvas.show_annotations.state(), state=self.canvas.show_annotations.state(),
) )
def shape_motion(self, x1: float, y1: float): def shape_motion(self, x1: float, y1: float) -> None:
self.canvas.coords(self.id, self.x1, self.y1, x1, y1) self.canvas.coords(self.id, self.x1, self.y1, x1, y1)
def shape_complete(self, x: float, y: float): def shape_complete(self, x: float, y: float) -> None:
self.canvas.organize() self.canvas.organize()
s = ShapeDialog(self.app, self) s = ShapeDialog(self.app, self)
s.show() s.show()
def disappear(self): def disappear(self) -> None:
self.canvas.delete(self.id) self.canvas.delete(self.id)
def motion(self, x_offset: float, y_offset: float): def motion(self, x_offset: float, y_offset: float) -> None:
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)
coords = self.canvas.coords(self.id) coords = self.canvas.coords(self.id)
@ -166,7 +166,7 @@ class Shape:
if self.text_id is not None: if self.text_id is not None:
self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset)
def delete(self): def delete(self) -> None:
logging.debug("Delete shape, id(%s)", self.id) logging.debug("Delete shape, id(%s)", self.id)
self.canvas.delete(self.id) self.canvas.delete(self.id)
self.canvas.delete(self.text_id) self.canvas.delete(self.text_id)

View file

@ -1,4 +1,5 @@
import enum import enum
from typing import Set
class ShapeType(enum.Enum): class ShapeType(enum.Enum):
@ -8,7 +9,7 @@ class ShapeType(enum.Enum):
TEXT = "text" TEXT = "text"
SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} SHAPES: Set[ShapeType] = {ShapeType.OVAL, ShapeType.RECTANGLE}
def is_draw_shape(shape_type: ShapeType) -> bool: def is_draw_shape(shape_type: ShapeType) -> bool:

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