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
cp setup.py.in setup.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
- name: isort
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
* Enhancements
* 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.
# 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
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 threading
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 netaddr
from core import utils
from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc
from core.api.grpc.configservices_pb2 import (
GetConfigServiceDefaultsRequest,
@ -31,6 +29,8 @@ from core.api.grpc.emane_pb2 import (
EmaneLinkRequest,
EmaneLinkResponse,
EmaneModelConfig,
EmanePathlossesRequest,
EmanePathlossesResponse,
GetEmaneConfigRequest,
GetEmaneConfigResponse,
GetEmaneEventChannelRequest,
@ -92,6 +92,7 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkRequest,
WlanLinkResponse,
)
from core.emulator.data import IpPrefixes
class InterfaceHelper:
@ -107,78 +108,29 @@ class InterfaceHelper:
: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.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix)
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 create_interface(
self, node_id: int, interface_id: int, name: str = None, mac: str = None
def create_iface(
self, node_id: int, iface_id: int, name: str = None, mac: str = None
) -> core_pb2.Interface:
"""
Creates interface data for linking nodes, using the nodes unique id for
generation, along with a random mac address, unless provided.
Create an interface protobuf object.
:param node_id: node id to create interface for
:param interface_id: interface id for interface
: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
:param iface_id: interface id
:param name: name of interface
:param mac: mac address for interface
:return: interface protobuf
"""
# 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()
iface_data = self.prefixes.gen_iface(node_id, name, mac)
return core_pb2.Interface(
id=interface_id,
name=name,
ip4=ip4,
ip4mask=ip4_mask,
ip6=ip6,
ip6mask=ip6_mask,
mac=str(mac),
id=iface_id,
name=iface_data.name,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
mac=iface_data.mac,
)
@ -225,10 +177,10 @@ class CoreGrpcClient:
:param address: grpc server address to connect to
"""
self.address = address
self.stub = None
self.channel = None
self.proxy = proxy
self.address: str = address
self.stub: Optional[core_pb2_grpc.CoreApiStub] = None
self.channel: Optional[grpc.Channel] = None
self.proxy: bool = proxy
def start_session(
self,
@ -287,6 +239,7 @@ class CoreGrpcClient:
:param session_id: id of session
:return: stop session response
:raises grpc.RpcError: when session doesn't exist
"""
request = core_pb2.StopSessionRequest(session_id=session_id)
return self.stub.StopSession(request)
@ -483,7 +436,7 @@ class CoreGrpcClient:
session_id: int,
handler: Callable[[core_pb2.Event], None],
events: List[core_pb2.Event] = None,
) -> Any:
) -> grpc.Channel:
"""
Listen for session events.
@ -500,7 +453,7 @@ class CoreGrpcClient:
def throughputs(
self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None]
) -> Any:
) -> grpc.Channel:
"""
Listen for throughput events with information for interfaces and bridges.
@ -515,17 +468,20 @@ class CoreGrpcClient:
return stream
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:
"""
Add node to session.
:param session_id: session id
:param node: node to add
:param source: source application
:return: response with node id
: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)
def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse:
@ -546,8 +502,8 @@ class CoreGrpcClient:
node_id: int,
position: core_pb2.Position = None,
icon: str = None,
source: str = None,
geo: core_pb2.Geo = None,
source: str = None,
) -> core_pb2.EditNodeResponse:
"""
Edit a node, currently only changes position.
@ -556,8 +512,8 @@ class CoreGrpcClient:
:param node_id: node id
:param position: position to set node to
: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 source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or node doesn't exist
"""
@ -571,20 +527,42 @@ class CoreGrpcClient:
)
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.
:param session_id: session id
:param node_id: node id
:param source: application source
:return: response with result of success or failure
: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)
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:
"""
Send command to a node and get the output.
@ -592,11 +570,17 @@ class CoreGrpcClient:
:param session_id: session id
:param node_id: node id
: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
:raises grpc.RpcError: when session or node doesn't exist
"""
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)
@ -633,91 +617,101 @@ class CoreGrpcClient:
def add_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
interface_one: core_pb2.Interface = None,
interface_two: core_pb2.Interface = None,
node1_id: int,
node2_id: int,
iface1: core_pb2.Interface = None,
iface2: core_pb2.Interface = None,
options: core_pb2.LinkOptions = None,
source: str = None,
) -> core_pb2.AddLinkResponse:
"""
Add a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param interface_one: node one interface data
:param interface_two: node two interface data
:param node1_id: node one id
:param node2_id: node two id
:param iface1: node one interface data
:param iface2: node two interface data
:param options: options for link (jitter, bandwidth, etc)
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist
"""
link = core_pb2.Link(
node_one_id=node_one_id,
node_two_id=node_two_id,
node1_id=node1_id,
node2_id=node2_id,
type=core_pb2.LinkType.WIRED,
interface_one=interface_one,
interface_two=interface_two,
iface1=iface1,
iface2=iface2,
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)
def edit_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
node1_id: int,
node2_id: int,
options: core_pb2.LinkOptions,
interface_one_id: int = None,
interface_two_id: int = None,
iface1_id: int = None,
iface2_id: int = None,
source: str = None,
) -> core_pb2.EditLinkResponse:
"""
Edit a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param node1_id: node one id
:param node2_id: node two id
:param options: options for link (jitter, bandwidth, etc)
:param interface_one_id: node one interface id
:param interface_two_id: node two interface id
:param iface1_id: node one interface id
:param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session or one of the nodes don't exist
"""
request = core_pb2.EditLinkRequest(
session_id=session_id,
node_one_id=node_one_id,
node_two_id=node_two_id,
node1_id=node1_id,
node2_id=node2_id,
options=options,
interface_one_id=interface_one_id,
interface_two_id=interface_two_id,
iface1_id=iface1_id,
iface2_id=iface2_id,
source=source,
)
return self.stub.EditLink(request)
def delete_link(
self,
session_id: int,
node_one_id: int,
node_two_id: int,
interface_one_id: int = None,
interface_two_id: int = None,
node1_id: int,
node2_id: int,
iface1_id: int = None,
iface2_id: int = None,
source: str = None,
) -> core_pb2.DeleteLinkResponse:
"""
Delete a link between nodes.
:param session_id: session id
:param node_one_id: node one id
:param node_two_id: node two id
:param interface_one_id: node one interface id
:param interface_two_id: node two interface id
:param node1_id: node one id
:param node2_id: node two id
:param iface1_id: node one interface id
:param iface2_id: node two interface id
:param source: application source
:return: response with result of success or failure
:raises grpc.RpcError: when session doesn't exist
"""
request = core_pb2.DeleteLinkRequest(
session_id=session_id,
node_one_id=node_one_id,
node_two_id=node_two_id,
interface_one_id=interface_one_id,
interface_two_id=interface_two_id,
node1_id=node1_id,
node2_id=node2_id,
iface1_id=iface1_id,
iface2_id=iface2_id,
source=source,
)
return self.stub.DeleteLink(request)
@ -1052,7 +1046,7 @@ class CoreGrpcClient:
return self.stub.GetEmaneModels(request)
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:
"""
Get emane model configuration for a node or a node's interface.
@ -1060,12 +1054,12 @@ class CoreGrpcClient:
:param session_id: session id
:param node_id: node id
: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
:raises grpc.RpcError: when session doesn't exist
"""
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)
@ -1074,8 +1068,8 @@ class CoreGrpcClient:
session_id: int,
node_id: int,
model: str,
config: Dict[str, str],
interface_id: int = -1,
config: Dict[str, str] = None,
iface_id: int = -1,
) -> SetEmaneModelConfigResponse:
"""
Set emane model configuration for a node or a node's interface.
@ -1084,12 +1078,12 @@ class CoreGrpcClient:
:param node_id: node id
:param model: emane model name
: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
:raises grpc.RpcError: when session doesn't exist
"""
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(
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:
"""
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
:raises grpc.RpcError: when session doesn't exist
"""
@ -1111,9 +1105,10 @@ class CoreGrpcClient:
"""
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
:return: nothing
:raises grpc.RpcError: when session doesn't exist
"""
request = core_pb2.SaveXmlRequest(session_id=session_id)
response = self.stub.SaveXml(request)
@ -1134,51 +1129,79 @@ class CoreGrpcClient:
return self.stub.OpenXml(request)
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:
"""
Helps broadcast wireless link/unlink between EMANE nodes.
:param session_id: session id
:param nem_one:
:param nem_two:
:param session_id: session to emane link
:param nem1: first nem for emane link
:param nem2: second nem for emane link
: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(
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)
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
a part of a CORE session.
:return: core_pb2.GetInterfacesResponse
:return: get interfaces response
"""
request = core_pb2.GetInterfacesRequest()
return self.stub.GetInterfaces(request)
def get_config_services(self) -> GetConfigServicesResponse:
"""
Retrieve all known config services.
:return: get config services response
"""
request = GetConfigServicesRequest()
return self.stub.GetConfigServices(request)
def get_config_service_defaults(
self, name: str
) -> GetConfigServiceDefaultsResponse:
"""
Retrieves config service default values.
:param name: name of service to get defaults for
:return: get config service defaults
"""
request = GetConfigServiceDefaultsRequest(name=name)
return self.stub.GetConfigServiceDefaults(request)
def get_node_config_service_configs(
self, session_id: int
) -> 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)
return self.stub.GetNodeConfigServiceConfigs(request)
def get_node_config_service(
self, session_id: int, node_id: int, name: str
) -> 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(
session_id=session_id, node_id=node_id, name=name
)
@ -1187,37 +1210,92 @@ class CoreGrpcClient:
def get_node_config_services(
self, session_id: int, node_id: int
) -> 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)
return self.stub.GetNodeConfigServices(request)
def set_node_config_service(
self, session_id: int, node_id: int, name: str, config: Dict[str, str]
) -> 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(
session_id=session_id, node_id=node_id, name=name, config=config
)
return self.stub.SetNodeConfigService(request)
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)
return self.stub.GetEmaneEventChannel(request)
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)
return self.stub.ExecuteScript(request)
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:
"""
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(
session_id=session_id,
wlan=wlan,
node_one=node_one,
node_two=node_two,
wlan=wlan_id,
node1_id=node1_id,
node2_id=node2_id,
linked=linked,
)
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:
"""
Open connection to server, must be closed manually.

View file

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

View file

@ -1,19 +1,19 @@
import logging
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.api.grpc import common_pb2, core_pb2
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes
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.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
_type = NodeTypes(node_proto.type)
options = NodeOptions(name=node_proto.name, model=node_proto.model)
options.icon = node_proto.icon
options.opaque = node_proto.opaque
options.image = node_proto.image
options.services = node_proto.services
options.config_services = node_proto.config_services
options = NodeOptions(
name=node_proto.name,
model=node_proto.model,
icon=node_proto.icon,
image=node_proto.image,
services=node_proto.services,
config_services=node_proto.config_services,
)
if node_proto.emane:
options.emane = node_proto.emane
if node_proto.server:
options.server = node_proto.server
position = node_proto.position
options.set_position(position.x, position.y)
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
def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
"""
Create interface data from interface proto.
:param interface_proto: interface proto
:param iface_proto: interface proto
:return: interface data
"""
interface = None
if interface_proto:
name = interface_proto.name
if name == "":
name = None
mac = interface_proto.mac
if mac == "":
mac = None
interface = InterfaceData(
_id=interface_proto.id,
iface_data = None
if iface_proto:
name = iface_proto.name if iface_proto.name else None
mac = iface_proto.mac if iface_proto.mac else None
ip4 = iface_proto.ip4 if iface_proto.ip4 else None
ip6 = iface_proto.ip6 if iface_proto.ip6 else None
iface_data = InterfaceData(
id=iface_proto.id,
name=name,
mac=mac,
ip4=interface_proto.ip4,
ip4_mask=interface_proto.ip4mask,
ip6=interface_proto.ip6,
ip6_mask=interface_proto.ip6mask,
ip4=ip4,
ip4_mask=iface_proto.ip4_mask,
ip6=ip6,
ip6_mask=iface_proto.ip6_mask,
)
return interface
return iface_data
def add_link_data(
link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
"""
Convert link proto to link interfaces and options data.
:param link_proto: link proto
:return: link interfaces and options
"""
interface_one = link_interface(link_proto.interface_one)
interface_two = link_interface(link_proto.interface_two)
link_type = None
link_type_value = link_proto.type
if link_type_value is not None:
link_type = LinkTypes(link_type_value)
options = LinkOptions(_type=link_type)
options_data = link_proto.options
if options_data:
options.delay = options_data.delay
options.bandwidth = options_data.bandwidth
options.per = options_data.per
options.dup = options_data.dup
options.jitter = options_data.jitter
options.mer = options_data.mer
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
iface1_data = link_iface(link_proto.iface1)
iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type)
options = LinkOptions()
options_proto = link_proto.options
if options_proto:
options.delay = options_proto.delay
options.bandwidth = options_proto.bandwidth
options.loss = options_proto.loss
options.dup = options_proto.dup
options.jitter = options_proto.jitter
options.mer = options_proto.mer
options.burst = options_proto.burst
options.mburst = options_proto.mburst
options.unidirectional = options_proto.unidirectional
options.key = options_proto.key
return iface1_data, iface2_data, options, link_type
def create_nodes(
@ -145,10 +137,10 @@ def create_links(
"""
funcs = []
for link_proto in link_protos:
node_one_id = link_proto.node_one_id
node_two_id = link_proto.node_two_id
interface_one, interface_two, options = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one, interface_two, options)
node1_id = link_proto.node1_id
node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node1_id, node2_id, iface1, iface2, options, link_type)
funcs.append((session.add_link, args, {}))
start = time.monotonic()
results, exceptions = utils.threadpool(funcs)
@ -169,10 +161,10 @@ def edit_links(
"""
funcs = []
for link_proto in link_protos:
node_one_id = link_proto.node_one_id
node_two_id = link_proto.node_two_id
interface_one, interface_two, options = add_link_data(link_proto)
args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options)
node1_id = link_proto.node1_id
node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto)
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
funcs.append((session.update_link, args, {}))
start = time.monotonic()
results, exceptions = utils.threadpool(funcs)
@ -194,7 +186,8 @@ def convert_value(value: Any) -> str:
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]:
"""
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
"""
links = []
for link_data in node.all_link_data():
link = convert_link(link_data)
links.append(link)
for link in node.links():
link_proto = convert_link(link)
links.append(link_proto)
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
:param node_id: node id
:param interface_id: interface id
:param iface_id: interface id
:return: EMANE model id
"""
if interface_id >= 0:
return node_id * 1000 + interface_id
if iface_id >= 0:
return node_id * 1000 + iface_id
else:
return node_id
@ -303,12 +296,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
:param _id: id to parse
:return: node id and interface id
"""
interface = -1
iface_id = -1
node_id = _id
if _id >= 1000:
interface = _id % 1000
iface_id = _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:
@ -318,47 +338,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
:param link_data: link to convert
:return: core protobuf Link
"""
interface_one = None
if link_data.interface1_id is not None:
interface_one = core_pb2.Interface(
id=link_data.interface1_id,
name=link_data.interface1_name,
mac=convert_value(link_data.interface1_mac),
ip4=convert_value(link_data.interface1_ip4),
ip4mask=link_data.interface1_ip4_mask,
ip6=convert_value(link_data.interface1_ip6),
ip6mask=link_data.interface1_ip6_mask,
)
interface_two = None
if link_data.interface2_id is not None:
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,
)
iface1 = None
if link_data.iface1 is not None:
iface1 = convert_iface(link_data.iface1)
iface2 = None
if link_data.iface2 is not None:
iface2 = convert_iface(link_data.iface2)
options = convert_link_options(link_data.options)
return core_pb2.Link(
type=link_data.link_type.value,
node_one_id=link_data.node1_id,
node_two_id=link_data.node2_id,
interface_one=interface_one,
interface_two=interface_two,
type=link_data.type.value,
node1_id=link_data.node1_id,
node2_id=link_data.node2_id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=link_data.network_id,
label=link_data.label,
@ -422,7 +414,7 @@ def service_configuration(session: Session, config: ServiceConfig) -> None:
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.
@ -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.
:param interface: interface to convert
:param node_id: id of node to convert interface for
:param iface: interface to convert
:return: interface proto
"""
net_id = None
if interface.net:
net_id = interface.net.id
ip4 = None
ip4mask = None
ip6 = None
ip6mask = None
for addr in interface.addrlist:
network = netaddr.IPNetwork(addr)
mask = network.prefixlen
ip = str(network.ip)
if netaddr.valid_ipv4(ip) and not ip4:
ip4 = ip
ip4mask = mask
elif netaddr.valid_ipv6(ip) and not ip6:
ip6 = ip
ip6mask = mask
if iface.node and iface.node.id == node_id:
_id = iface.node_id
else:
_id = iface.net_id
net_id = iface.net.id if iface.net else None
node_id = iface.node.id if iface.node else None
net2_id = iface.othernet.id if iface.othernet else None
ip4_net = iface.get_ip4()
ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None
ip6_net = iface.get_ip6()
ip6 = str(ip6_net.ip) if ip6_net else None
ip6_mask = ip6_net.prefixlen if ip6_net else None
mac = str(iface.mac) if iface.mac else None
return core_pb2.Interface(
id=interface.netindex,
netid=net_id,
name=interface.name,
mac=str(interface.hwaddr),
mtu=interface.mtu,
flowid=interface.flow_id,
id=_id,
net_id=net_id,
net2_id=net2_id,
node_id=node_id,
name=iface.name,
mac=mac,
mtu=iface.mtu,
flow_id=iface.flow_id,
ip4=ip4,
ip4mask=ip4mask,
ip4_mask=ip4_mask,
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 time
from concurrent import futures
from typing import Type
from typing import Iterable, Optional, Pattern, Type
import grpc
from grpc import ServicerContext
@ -39,6 +39,8 @@ from core.api.grpc.core_pb2 import ExecuteScriptResponse
from core.api.grpc.emane_pb2 import (
EmaneLinkRequest,
EmaneLinkResponse,
EmanePathlossesRequest,
EmanePathlossesResponse,
GetEmaneConfigRequest,
GetEmaneConfigResponse,
GetEmaneEventChannelRequest,
@ -106,18 +108,17 @@ from core.api.grpc.wlan_pb2 import (
WlanLinkResponse,
)
from core.emulator.coreemu import CoreEmu
from core.emulator.data import LinkData
from core.emulator.emudata import LinkOptions, NodeOptions
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
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
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
_INTERFACE_REGEX = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -129,9 +130,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def __init__(self, coreemu: CoreEmu) -> None:
super().__init__()
self.coreemu = coreemu
self.running = True
self.server = None
self.coreemu: CoreEmu = coreemu
self.running: bool = True
self.server: Optional[grpc.Server] = None
atexit.register(self._exit_handler)
def _exit_handler(self) -> None:
@ -244,7 +245,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
config = session.emane.get_configs()
config.update(request.emane_config)
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)
# wlan configs
@ -542,10 +543,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes = []
for _id in session.nodes:
node = session.nodes[_id]
if not isinstance(node.id, int):
continue
node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto)
if not isinstance(node, PtpNet):
node_proto = grpcutils.get_node_proto(session, node)
nodes.append(node_proto)
node_links = get_links(node)
links.extend(node_links)
@ -623,16 +623,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
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)
if session.id != session_id:
continue
interface_throughput = (
throughputs_event.interface_throughputs.add()
)
interface_throughput.node_id = node_id
interface_throughput.interface_id = interface_id
interface_throughput.throughput = throughput
iface_throughput = throughputs_event.iface_throughputs.add()
iface_throughput.node_id = node_id
iface_throughput.iface_id = iface_id
iface_throughput.throughput = throughput
elif key.startswith("b."):
try:
key = key.split(".")
@ -669,6 +667,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
_type, _id, options = grpcutils.add_node_data(request.node)
_class = session.get_node_class(_type)
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)
def GetNode(
@ -684,13 +684,49 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get node: %s", request)
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, NodeBase)
interfaces = []
for interface_id in node._netif:
interface = node._netif[interface_id]
interface_proto = grpcutils.interface_to_proto(interface)
interfaces.append(interface_proto)
ifaces = []
for iface_id in node.ifaces:
iface = node.ifaces[iface_id]
iface_proto = grpcutils.iface_to_proto(request.node_id, iface)
ifaces.append(iface_proto)
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(
self, request: core_pb2.EditNodeRequest, context: ServicerContext
@ -705,8 +741,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("edit node: %s", request)
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, NodeBase)
options = NodeOptions()
options.icon = request.icon
options = NodeOptions(icon=request.icon)
if request.HasField("position"):
x = request.position.x
y = request.position.y
@ -741,7 +776,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("delete node: %s", request)
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)
def NodeCommand(
@ -758,10 +798,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, CoreNode)
try:
output = node.cmd(request.command)
output = node.cmd(request.command, request.wait, request.shell)
return_code = 0
except CoreCommandError as e:
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(
self, request: core_pb2.GetNodeTerminalRequest, context: ServicerContext
@ -806,27 +848,42 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: add-link response
"""
logging.debug("add link: %s", request)
# validate session and nodes
session = self.get_session(request.session_id, context)
self.get_node(session, request.link.node_one_id, context, NodeBase)
self.get_node(session, request.link.node_two_id, context, NodeBase)
node_one_id = request.link.node_one_id
node_two_id = request.link.node_two_id
interface_one, interface_two, options = grpcutils.add_link_data(request.link)
node_one_interface, node_two_interface = session.add_link(
node_one_id, node_two_id, interface_one, interface_two, link_options=options
node1_id = request.link.node1_id
node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase)
self.get_node(session, node2_id, context, NodeBase)
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
request.link
)
interface_one_proto = None
interface_two_proto = None
if node_one_interface:
interface_one_proto = grpcutils.interface_to_proto(node_one_interface)
if node_two_interface:
interface_two_proto = grpcutils.interface_to_proto(node_two_interface)
node1_iface, node2_iface = session.add_link(
node1_id, node2_id, iface1_data, iface2_data, options, link_type
)
iface1_data = None
if node1_iface:
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(
result=True,
interface_one=interface_one_proto,
interface_two=interface_two_proto,
result=True, iface1=iface1_proto, iface2=iface2_proto
)
def EditLink(
@ -841,26 +898,37 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("edit link: %s", request)
session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id
node_two_id = request.node_two_id
interface_one_id = request.interface_one_id
interface_two_id = request.interface_two_id
options_data = request.options
link_options = LinkOptions()
link_options.delay = options_data.delay
link_options.bandwidth = options_data.bandwidth
link_options.per = options_data.per
link_options.dup = options_data.dup
link_options.jitter = options_data.jitter
link_options.mer = options_data.mer
link_options.burst = options_data.burst
link_options.mburst = options_data.mburst
link_options.unidirectional = options_data.unidirectional
link_options.key = options_data.key
link_options.opaque = options_data.opaque
session.update_link(
node_one_id, node_two_id, interface_one_id, interface_two_id, link_options
node1_id = request.node1_id
node2_id = request.node2_id
iface1_id = request.iface1_id
iface2_id = request.iface2_id
options_proto = request.options
options = LinkOptions(
delay=options_proto.delay,
bandwidth=options_proto.bandwidth,
loss=options_proto.loss,
dup=options_proto.dup,
jitter=options_proto.jitter,
mer=options_proto.mer,
burst=options_proto.burst,
mburst=options_proto.mburst,
unidirectional=options_proto.unidirectional,
key=options_proto.key,
)
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)
def DeleteLink(
@ -875,13 +943,23 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("delete link: %s", request)
session = self.get_session(request.session_id, context)
node_one_id = request.node_one_id
node_two_id = request.node_two_id
interface_one_id = request.interface_one_id
interface_two_id = request.interface_two_id
session.delete_link(
node_one_id, node_two_id, interface_one_id, interface_two_id
node1_id = request.node1_id
node2_id = request.node2_id
iface1_id = request.iface1_id
iface2_id = request.iface2_id
session.delete_link(node1_id, node2_id, iface1_id, iface2_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)
def GetHooks(
@ -897,8 +975,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get hooks: %s", request)
session = self.get_session(request.session_id, context)
hooks = []
for state in session._hooks:
state_hooks = session._hooks[state]
for state in session.hooks:
state_hooks = session.hooks[state]
for file_name, file_data in state_hooks:
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
hooks.append(hook)
@ -1265,13 +1343,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("set wlan config: %s", request)
session = self.get_session(request.session_id, context)
wlan_config = request.wlan_config
session.mobility.set_model_config(
wlan_config.node_id, BasicRangeModel.name, wlan_config.config
)
node_id = request.wlan_config.node_id
config = request.wlan_config.config
session.mobility.set_model_config(node_id, BasicRangeModel.name, config)
if session.state == EventTypes.RUNTIME_STATE:
node = self.get_node(session, wlan_config.node_id, context, WlanNode)
node.updatemodel(wlan_config.config)
node = self.get_node(session, node_id, context, WlanNode)
node.updatemodel(config)
return SetWlanConfigResponse(result=True)
def GetEmaneConfig(
@ -1339,7 +1416,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("get emane model config: %s", request)
session = self.get_session(request.session_id, context)
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)
config = get_config_options(current_config, model)
return GetEmaneModelConfigResponse(config=config)
@ -1358,7 +1435,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
logging.debug("set emane model config: %s", request)
session = self.get_session(request.session_id, context)
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)
return SetEmaneModelConfigResponse(result=True)
@ -1387,12 +1464,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
model = session.emane.models[model_name]
current_config = session.emane.get_model_config(_id, model_name)
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(
node_id=node_id,
model=model_name,
interface=interface,
config=config,
node_id=node_id, model=model_name, iface_id=iface_id, config=config
)
configs.append(model_config)
return GetEmaneModelConfigsResponse(configs=configs)
@ -1457,16 +1531,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:param context: context object
:return: get-interfaces response that has all the system's interfaces
"""
interfaces = []
for interface in os.listdir("/sys/class/net"):
if (
interface.startswith("b.")
or interface.startswith("veth")
or interface == "lo"
):
ifaces = []
for iface in os.listdir("/sys/class/net"):
if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
continue
interfaces.append(interface)
return core_pb2.GetInterfacesResponse(interfaces=interfaces)
ifaces.append(iface)
return core_pb2.GetInterfacesResponse(ifaces=ifaces)
def EmaneLink(
self, request: EmaneLinkRequest, context: ServicerContext
@ -1480,30 +1550,30 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("emane link: %s", request)
session = self.get_session(request.session_id, context)
nem_one = request.nem_one
emane_one, netif = session.emane.nemlookup(nem_one)
if not emane_one or not netif:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem_one} not found")
node_one = netif.node
nem1 = request.nem1
iface1 = session.emane.get_iface(nem1)
if not iface1:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
node1 = iface1.node
nem_two = request.nem_two
emane_two, netif = session.emane.nemlookup(nem_two)
if not emane_two or not netif:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem_two} not found")
node_two = netif.node
nem2 = request.nem2
iface2 = session.emane.get_iface(nem2)
if not iface2:
context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
node2 = iface2.node
if emane_one.id == emane_two.id:
if iface1.net == iface2.net:
if request.linked:
flag = MessageFlags.ADD
else:
flag = MessageFlags.DELETE
color = session.get_link_color(emane_one.id)
color = session.get_link_color(iface1.net.id)
link = LinkData(
message_type=flag,
link_type=LinkTypes.WIRELESS,
node1_id=node_one.id,
node2_id=node_two.id,
network_id=emane_one.id,
type=LinkTypes.WIRELESS,
node1_id=node1.id,
node2_id=node2.id,
network_id=iface1.net.id,
color=color,
)
session.broadcast_link(link)
@ -1700,20 +1770,34 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
grpc.StatusCode.NOT_FOUND,
f"wlan node {request.wlan} does not using BasicRangeModel",
)
n1 = self.get_node(session, request.node_one, context, CoreNode)
n2 = self.get_node(session, request.node_two, context, CoreNode)
n1_netif, n2_netif = None, None
for net, netif1, netif2 in n1.commonnets(n2):
node1 = self.get_node(session, request.node1_id, context, CoreNode)
node2 = self.get_node(session, request.node2_id, context, CoreNode)
node1_iface, node2_iface = None, None
for net, iface1, iface2 in node1.commonnets(node2):
if net == wlan:
n1_netif = netif1
n2_netif = netif2
node1_iface = iface1
node2_iface = iface2
break
result = False
if n1_netif and n2_netif:
if node1_iface and node2_iface:
if request.linked:
wlan.link(n1_netif, n2_netif)
wlan.link(node1_iface, node2_iface)
else:
wlan.unlink(n1_netif, n2_netif)
wlan.model.sendlinkmsg(n1_netif, n2_netif, unlink=not request.linked)
wlan.unlink(node1_iface, node2_iface)
wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
result = True
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.DELAY.value: CoreTlvDataUint64,
LinkTlvs.BANDWIDTH.value: CoreTlvDataUint64,
LinkTlvs.PER.value: CoreTlvDataString,
LinkTlvs.LOSS.value: CoreTlvDataString,
LinkTlvs.DUP.value: CoreTlvDataString,
LinkTlvs.JITTER.value: CoreTlvDataUint64,
LinkTlvs.MER.value: CoreTlvDataUint16,
@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
LinkTlvs.KEY.value: CoreTlvDataUint32,
LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16,
LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr,
LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16,
LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
LinkTlvs.OPAQUE.value: CoreTlvDataString,
@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv):
ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
ConfigTlvs.GROUPS.value: CoreTlvDataString,
ConfigTlvs.SESSION.value: CoreTlvDataString,
ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16,
ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16,
ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
ConfigTlvs.OPAQUE.value: CoreTlvDataString,
}

View file

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

View file

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

View file

@ -4,7 +4,7 @@ Common support for configurable CORE objects.
import logging
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.emulator.enumerations import ConfigDataTypes
@ -29,9 +29,9 @@ class ConfigGroup:
:param start: configurations start index for this group
:param stop: configurations stop index for this group
"""
self.name = name
self.start = start
self.stop = stop
self.name: str = name
self.start: int = start
self.stop: int = stop
class Configuration:
@ -56,18 +56,21 @@ class Configuration:
:param default: default value for configuration
:param options: list options if this is a configuration with a combobox
"""
self.id = _id
self.type = _type
self.default = default
self.id: str = _id
self.type: ConfigDataTypes = _type
self.default: str = default
if not options:
options = []
self.options = options
self.options: List[str] = options
if not label:
label = _id
self.label = label
self.label: str = label
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:
@ -75,9 +78,9 @@ class ConfigurableOptions:
Provides a base for defining configuration options within CORE.
"""
name = None
bitmap = None
options = []
name: Optional[str] = None
bitmap: Optional[str] = None
options: List[Configuration] = []
@classmethod
def configurations(cls) -> List[Configuration]:
@ -115,8 +118,8 @@ class ConfigurableManager:
nodes.
"""
_default_node = -1
_default_type = _default_node
_default_node: int = -1
_default_type: int = _default_node
def __init__(self) -> None:
"""
@ -136,7 +139,8 @@ class ConfigurableManager:
"""
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
"""
if not node_id:
@ -222,7 +226,7 @@ class ConfigurableManager:
result = node_configs.get(config_type)
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.
@ -242,8 +246,8 @@ class ModelManager(ConfigurableManager):
Creates a ModelManager object.
"""
super().__init__()
self.models = {}
self.node_models = {}
self.models: Dict[str, Any] = {}
self.node_models: Dict[int, str] = {}
def set_model_config(
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.nodes.base import CoreNode
TEMPLATES_DIR = "templates"
TEMPLATES_DIR: str = "templates"
class ConfigServiceMode(enum.Enum):
@ -33,10 +33,10 @@ class ConfigService(abc.ABC):
"""
# 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
validation_timer = 5
validation_timer: int = 5
def __init__(self, node: CoreNode) -> None:
"""
@ -44,13 +44,13 @@ class ConfigService(abc.ABC):
:param node: node this service is assigned to
"""
self.node = node
self.node: CoreNode = node
class_file = inspect.getfile(self.__class__)
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
self.templates = TemplateLookup(directories=templates_path)
self.config = {}
self.custom_templates = {}
self.custom_config = {}
self.templates: TemplateLookup = TemplateLookup(directories=templates_path)
self.config: Dict[str, Configuration] = {}
self.custom_templates: Dict[str, str] = {}
self.custom_config: Dict[str, str] = {}
configs = self.default_configs[:]
self._define_config(configs)

View file

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

View file

@ -1,6 +1,6 @@
import logging
import pathlib
from typing import List, Type
from typing import Dict, List, Type
from core import utils
from core.configservice.base import ConfigService
@ -16,7 +16,7 @@ class ConfigServiceManager:
"""
Create a ConfigServiceManager instance.
"""
self.services = {}
self.services: Dict[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}")
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.
@ -40,7 +40,9 @@ class ConfigServiceManager:
:raises CoreError: when service is a duplicate or has unmet executables
"""
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
if name in self.services:
@ -50,10 +52,8 @@ class ConfigServiceManager:
for executable in service.executables:
try:
utils.which(executable, required=True)
except ValueError:
raise CoreError(
f"service({service.name}) missing executable {executable}"
)
except CoreError as e:
raise CoreError(f"config service({service.name}): {e}")
# make service available
self.services[name] = service
@ -73,7 +73,6 @@ class ConfigServiceManager:
logging.debug("loading config services from: %s", subdir)
services = utils.load_classes(str(subdir), ConfigService)
for service in services:
logging.debug("found service: %s", service)
try:
self.add(service)
except CoreError as e:

View file

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

View file

@ -1,5 +1,5 @@
% for ifc, ip4s, ip6s, is_control in interfaces:
interface ${ifc.name}
% for iface, ip4s, ip6s, is_control in ifaces:
interface ${iface.name}
% if want_ip4:
% for addr in ip4s:
ip address ${addr}
@ -12,7 +12,7 @@ interface ${ifc.name}
% endif
% if not is_control:
% 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}
% endfor
% endfor

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,8 @@
<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>
<ul>
% for ifc in interfaces:
<li>${ifc.name} - ${ifc.addrlist}</li>
% for iface in ifaces:
<li>${iface.name} - ${iface.addrlist}</li>
% endfor
</ul>
</body>

View file

@ -1,19 +1,3 @@
from core.utils import which
COREDPY_VERSION = "@PACKAGE_VERSION@"
CORE_CONF_DIR = "@CORE_CONF_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
"""
from typing import List, Set
from core.config import Configuration
from core.emane import emanemodel
@ -8,14 +9,14 @@ from core.emulator.enumerations import ConfigDataTypes
class EmaneBypassModel(emanemodel.EmaneModel):
name = "emane_bypass"
name: str = "emane_bypass"
# values to ignore, when writing xml files
config_ignore = {"none"}
config_ignore: Set[str] = {"none"}
# mac definitions
mac_library = "bypassmaclayer"
mac_config = [
mac_library: str = "bypassmaclayer"
mac_config: List[Configuration] = [
Configuration(
_id="none",
_type=ConfigDataTypes.BOOL,
@ -25,8 +26,8 @@ class EmaneBypassModel(emanemodel.EmaneModel):
]
# phy definitions
phy_library = "bypassphylayer"
phy_config = []
phy_library: str = "bypassphylayer"
phy_config: List[Configuration] = []
@classmethod
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.emane import emanemanifest, emanemodel
from core.emane.nodes import EmaneNet
from core.emulator.enumerations import TransportType
from core.emulator.data import LinkOptions
from core.nodes.interface import CoreInterface
from core.xml import emanexml
@ -21,6 +20,7 @@ except ImportError:
try:
from emanesh.events.commeffectevent import CommEffectEvent
except ImportError:
CommEffectEvent = None
logging.debug("compatible emane python bindings not installed")
@ -37,16 +37,15 @@ def convert_none(x: float) -> int:
class EmaneCommEffectModel(emanemodel.EmaneModel):
name = "emane_commeffect"
shim_library = "commeffectshim"
shim_xml = "commeffectshim.xml"
shim_defaults = {}
config_shim = []
name: str = "emane_commeffect"
shim_library: str = "commeffectshim"
shim_xml: str = "commeffectshim.xml"
shim_defaults: Dict[str, str] = {}
config_shim: List[Configuration] = []
# comm effect does not need the default phy and external configurations
phy_config = []
external_config = []
phy_config: List[Configuration] = []
external_config: List[Configuration] = []
@classmethod
def load(cls, emane_prefix: str) -> None:
@ -61,9 +60,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
def config_groups(cls) -> List[ConfigGroup]:
return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
def build_xml_files(
self, config: Dict[str, str], interface: CoreInterface = None
) -> None:
def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None:
"""
Build the necessary nem and commeffect XMLs in the given path.
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.
: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
"""
# retrieve xml names
nem_name = emanexml.nem_file_name(self, interface)
shim_name = emanexml.shim_file_name(self, interface)
# create and write nem document
nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
transport_type = TransportType.VIRTUAL
if interface and interface.transport_type == TransportType.RAW:
transport_type = TransportType.RAW
transport_file = emanexml.transport_file_name(self.id, transport_type)
etree.SubElement(nem_element, "transport", definition=transport_file)
transport_name = emanexml.transport_file_name(iface)
etree.SubElement(nem_element, "transport", definition=transport_name)
# 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)
nem_file = os.path.join(self.session.session_dir, nem_name)
emanexml.create_file(nem_element, "nem", nem_file)
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
# create and write shim document
shim_element = etree.Element(
@ -109,19 +99,13 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
ff = config["filterfile"]
if ff.strip() != "":
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)
emanexml.create_file(shim_element, "shim", shim_file)
# create transport xml
emanexml.create_transport_xml(iface, config)
def linkconfig(
self,
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None:
"""
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)
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)
return
# TODO: batch these into multiple events per transmission
# TODO: may want to split out seconds portion of delay and jitter
event = CommEffectEvent()
emane_node = self.session.get_node(self.id, EmaneNet)
nemid = emane_node.getnemid(netif)
nemid2 = emane_node.getnemid(netif2)
mbw = bw
nem1 = self.session.emane.get_nem_id(iface)
nem2 = self.session.emane.get_nem_id(iface2)
logging.info("sending comm effect event")
event.append(
nemid,
latency=convert_none(delay),
jitter=convert_none(jitter),
loss=convert_none(loss),
duplicate=convert_none(duplicate),
unicast=int(convert_none(bw)),
broadcast=int(convert_none(mbw)),
nem1,
latency=convert_none(options.delay),
jitter=convert_none(options.jitter),
loss=convert_none(options.loss),
duplicate=convert_none(options.dup),
unicast=int(convert_none(options.bandwidth)),
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 threading
from collections import OrderedDict
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from core import utils
@ -28,16 +29,14 @@ from core.emulator.enumerations import (
)
from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import CtrlNet
from core.nodes.physical import Rj45Node
from core.nodes.interface import CoreInterface, TunTap
from core.xml import emanexml
if TYPE_CHECKING:
from core.emulator.session import Session
try:
from emane.events import EventService
from emane.events import EventService, PathlossEvent
from emane.events import LocationEvent
from emane.events.eventserviceexception import EventServiceException
except ImportError:
@ -48,6 +47,7 @@ except ImportError:
except ImportError:
EventService = None
LocationEvent = None
PathlossEvent = None
EventServiceException = None
logging.debug("compatible emane python bindings not installed")
@ -62,6 +62,12 @@ DEFAULT_EMANE_PREFIX = "/usr"
DEFAULT_DEV = "ctrl0"
class EmaneState(Enum):
SUCCESS = 0
NOT_NEEDED = 1
NOT_READY = 2
class EmaneManager(ModelManager):
"""
EMANE controller object. Lives in a Session instance and is used for
@ -69,11 +75,11 @@ class EmaneManager(ModelManager):
controlling the EMANE daemons.
"""
name = "emane"
config_type = RegisterTlvs.EMULATION_SERVER
SUCCESS, NOT_NEEDED, NOT_READY = (0, 1, 2)
EVENTCFGVAR = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL = 3
name: str = "emane"
config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER
NOT_READY: int = 2
EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG"
DEFAULT_LOG_LEVEL: int = 3
def __init__(self, session: "Session") -> None:
"""
@ -83,74 +89,71 @@ class EmaneManager(ModelManager):
:return: nothing
"""
super().__init__()
self.session = session
self._emane_nets = {}
self._emane_node_lock = threading.Lock()
self.session: "Session" = session
self.nems_to_ifaces: Dict[int, CoreInterface] = {}
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
self.platformport = self.session.options.get_config_int(
self.platformport: int = self.session.options.get_config_int(
"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
)
self.doeventloop = False
self.eventmonthread = None
self.doeventloop: bool = False
self.eventmonthread: Optional[threading.Thread] = None
# 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())
# link monitor
self.link_monitor = EmaneLinkMonitor(self)
self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self)
self.service = None
self.eventchannel = None
self.event_device = None
self.service: Optional[EventService] = None
self.eventchannel: Optional[Tuple[str, int, str]] = None
self.event_device: Optional[str] = None
self.emane_check()
def getifcconfig(
self, node_id: int, interface: CoreInterface, model_name: str
def next_nem_id(self) -> int:
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]:
"""
Retrieve interface configuration or node configuration if not provided.
Retrieve configuration for a given interface.
:param node_id: node id
:param interface: node interface
:param model_name: model to get configuration for
:return: node/interface model configuration
:param emane_net: emane network the interface is connected to
:param iface: interface running emane
:return: net, node, or interface model configuration
"""
# use the network-wide config values or interface(NEM)-specific values?
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
# note here that using ifc.node.id as key allows for only one type
# of each model per node;
# TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:ifc.name"
# (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface)
key = 1000 * interface.node.id
if interface.netindex is not None:
key += interface.netindex
# try retrieve interface specific configuration, avoid getting defaults
config = self.get_configs(node_id=key, config_type=model_name)
# otherwise retrieve the interfaces node configuration, avoid using defaults
if not config:
config = self.get_configs(
node_id=interface.node.id, config_type=model_name
)
# get non interface config, when none found
if not config:
# with EMANE 0.9.2+, we need an extra NEM XML from
# model.buildnemxmlfiles(), so defaults are returned here
config = self.get_configs(node_id=node_id, config_type=model_name)
return config
model_name = emane_net.model.name
# don"t use default values when interface config is the same as net
# note here that using iface.node.id as key allows for only one type
# of each model per node;
# TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:iface.name"
# (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface)
key = 1000 * iface.node.id
if iface.node_id is not None:
key += iface.node_id
# try retrieve interface specific configuration, avoid getting defaults
config = self.get_configs(node_id=key, config_type=model_name)
# otherwise retrieve the interfaces node configuration, avoid using defaults
if not config:
config = self.get_configs(node_id=iface.node.id, config_type=model_name)
# get non interface config, when none found
if not config:
# with EMANE 0.9.2+, we need an extra NEM XML from
# model.buildnemxmlfiles(), so defaults are returned here
config = self.get_configs(node_id=emane_net.id, config_type=model_name)
return config
def config_reset(self, node_id: int = None) -> None:
super().config_reset(node_id)
@ -162,23 +165,24 @@ class EmaneManager(ModelManager):
:return: nothing
"""
try:
# check for emane
args = "emane --version"
emane_version = utils.cmd(args)
logging.info("using EMANE: %s", emane_version)
self.session.distributed.execute(lambda x: x.remote_cmd(args))
# load default emane models
self.load_models(EMANE_MODELS)
# load custom models
custom_models_path = self.session.options.get_config("emane_models_dir")
if custom_models_path:
emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models)
except CoreCommandError:
# check for emane
path = utils.which("emane", required=False)
if not path:
logging.info("emane is not installed")
return
# get version
emane_version = utils.cmd("emane --version")
logging.info("using emane: %s", emane_version)
# load default emane models
self.load_models(EMANE_MODELS)
# load custom models
custom_models_path = self.session.options.get_config("emane_models_dir")
if custom_models_path:
emane_models = utils.load_classes(custom_models_path, EmaneModel)
self.load_models(emane_models)
def deleteeventservice(self) -> None:
if self.service:
@ -249,8 +253,8 @@ class EmaneManager(ModelManager):
"""
with self._emane_node_lock:
if emane_net.id in self._emane_nets:
raise KeyError(
f"non-unique EMANE object id {emane_net.id} for {emane_net}"
raise CoreError(
f"duplicate emane network({emane_net.id}): {emane_net.name}"
)
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,
e.g. containers having one or more radio interfaces.
"""
# assumes self._objslock already held
nodes = set()
for emane_net in self._emane_nets.values():
for netif in emane_net.netifs():
nodes.add(netif.node)
for iface in emane_net.get_ifaces():
nodes.add(iface.node)
return nodes
def setup(self) -> int:
def setup(self) -> EmaneState:
"""
Setup duties for EMANE manager.
@ -274,9 +277,7 @@ class EmaneManager(ModelManager):
instantiation
"""
logging.debug("emane setup")
# TODO: drive this from the session object
with self.session._nodes_lock:
with self.session.nodes_lock:
for node_id in self.session.nodes:
node = self.session.nodes[node_id]
if isinstance(node, EmaneNet):
@ -284,10 +285,9 @@ class EmaneManager(ModelManager):
"adding emane node: id(%s) name(%s)", node.id, node.name
)
self.add_node(node)
if not self._emane_nets:
logging.debug("no emane nodes in session")
return EmaneManager.NOT_NEEDED
return EmaneState.NOT_NEEDED
# check if bindings were installed
if EventService is None:
@ -303,7 +303,7 @@ class EmaneManager(ModelManager):
"EMANE cannot start, check core config. invalid OTA device provided: %s",
otadev,
)
return EmaneManager.NOT_READY
return EmaneState.NOT_READY
self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False
@ -315,19 +315,18 @@ class EmaneManager(ModelManager):
logging.debug("emane event service device index: %s", netidx)
if netidx < 0:
logging.error(
"EMANE cannot start, check core config. invalid event service device: %s",
"emane cannot start due to invalid event service device: %s",
eventdev,
)
return EmaneManager.NOT_READY
return EmaneState.NOT_READY
self.session.add_remove_control_net(
net_index=netidx, remove=False, conf_required=False
)
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
and start the daemons.
@ -336,39 +335,63 @@ class EmaneManager(ModelManager):
instantiation
"""
self.reset()
r = self.setup()
# NOT_NEEDED or NOT_READY
if r != EmaneManager.SUCCESS:
return r
nems = []
status = self.setup()
if status != EmaneState.SUCCESS:
return status
self.starteventmonitor()
self.buildeventservicexml()
with self._emane_node_lock:
self.buildxml()
self.starteventmonitor()
if self.numnems() > 0:
self.startdaemons()
self.installnetifs()
for node_id in self._emane_nets:
emane_node = self._emane_nets[node_id]
for netif in emane_node.netifs():
nems.append(
(netif.node.name, netif.name, emane_node.getnemid(netif))
)
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")
logging.info("emane building xmls...")
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
if not emane_net.model:
logging.error("emane net(%s) has no model", emane_net.name)
continue
for iface in emane_net.get_ifaces():
self.start_iface(emane_net, iface)
if self.links_enabled():
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:
return self.get_config("link_enabled") == "1"
@ -379,18 +402,15 @@ class EmaneManager(ModelManager):
"""
if not self.genlocationevents():
return
with self._emane_node_lock:
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
logging.debug(
"post startup for emane node: %s - %s",
emane_node.id,
emane_node.name,
"post startup for emane node: %s - %s", emane_net.id, emane_net.name
)
emane_node.model.post_startup()
for netif in emane_node.netifs():
netif.setposition()
emane_net.model.post_startup()
for iface in emane_net.get_ifaces():
iface.setposition()
def reset(self) -> None:
"""
@ -399,13 +419,8 @@ class EmaneManager(ModelManager):
"""
with self._emane_node_lock:
self._emane_nets.clear()
self.platformport = self.session.options.get_config_int(
"emane_platform_port", 8100
)
self.transformport = self.session.options.get_config_int(
"emane_transform_port", 8200
)
self.nems_to_ifaces.clear()
self.ifaces_to_nems.clear()
def shutdown(self) -> None:
"""
@ -417,44 +432,27 @@ class EmaneManager(ModelManager):
logging.info("stopping EMANE daemons")
if self.links_enabled():
self.link_monitor.stop()
self.deinstallnetifs()
self.deinstall_ifaces()
self.stopdaemons()
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:
"""
Associate EMANE model classes with EMANE network nodes.
"""
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)
# skip nodes that already have a model set
if emane_node.model:
if emane_net.model:
logging.debug(
"node(%s) already has model(%s)",
emane_node.id,
emane_node.model.name,
"node(%s) already has model(%s)", emane_net.id, emane_net.model.name
)
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)
if not model_name:
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)
logging.debug("setting emane model(%s) config(%s)", model_name, config)
model_class = self.models[model_name]
emane_node.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
emane_net.setmodel(model_class, config)
def get_nem_link(
self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
) -> Optional[LinkData]:
emane1, netif = self.nemlookup(nem1)
if not emane1 or not netif:
iface1 = self.get_iface(nem1)
if not iface1:
logging.error("invalid nem: %s", nem1)
return None
node1 = netif.node
emane2, netif = self.nemlookup(nem2)
if not emane2 or not netif:
node1 = iface1.node
iface2 = self.get_iface(nem2)
if not iface2:
logging.error("invalid nem: %s", nem2)
return None
node2 = netif.node
color = self.session.get_link_color(emane1.id)
node2 = iface2.node
if iface1.net != iface2.net:
return None
emane_net = iface1.net
color = self.session.get_link_color(emane_net.id)
return LinkData(
message_type=flags,
type=LinkTypes.WIRELESS,
node1_id=node1.id,
node2_id=node2.id,
network_id=emane1.id,
link_type=LinkTypes.WIRELESS,
network_id=emane_net.id,
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:
"""
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.
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")
realtime = self.session.options.get_config_bool("emane_realtime", default=True)
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)
emanecmd = f"emane -d -l {loglevel}"
if realtime:
emanecmd += " -r"
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice")
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
node = iface.node
if iface.is_virtual():
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
eventgroup, _eventport = self.get_config("eventservicegroup").split(":")
eventdev = self.get_config("eventservicedevice")
eventservicenetidx = self.session.get_control_net_index(eventdev)
# 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
)
if otanetidx > 0:
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
)
if eventservicenetidx >= 0:
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
)
# 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)
# multicast route is also needed for event data if on control network
if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev)
# start emane
log_file = os.path.join(path, f"emane{n}.log")
platform_xml = os.path.join(path, f"platform{n}.xml")
log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
platform_xml = os.path.join(node.nodedir, f"{iface.name}-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.debug("node(%s) emane daemon output: %s", node.name, output)
if not run_emane_on_host:
return
path = self.session.session_dir
log_file = os.path.join(path, "emane.log")
platform_xml = os.path.join(path, "platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
utils.cmd(emanecmd, cwd=path)
self.session.distributed.execute(lambda x: x.remote_cmd(emanecmd, cwd=path))
logging.info("host emane daemon running: %s", emanecmd)
else:
path = self.session.session_dir
log_file = os.path.join(path, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
node.host_cmd(emanecmd, cwd=path)
logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
def stopdaemons(self) -> None:
"""
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_transortd = "killall -q emanetransportd"
stop_emane_on_host = False
for node in self.getnodes():
if isinstance(node, Rj45Node):
stop_emane_on_host = True
continue
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
for iface in emane_net.get_ifaces():
node = iface.node
if not node.up:
continue
if iface.is_raw():
node.host_cmd(kill_emaned, wait=False)
else:
node.cmd(kill_emaned, wait=False)
if node.up:
node.cmd(kill_emaned, wait=False)
# TODO: RJ45 node
def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
config = self.get_iface_config(emane_net, iface)
external = config.get("external", "0")
if isinstance(iface, TunTap) and external == "0":
iface.set_ips()
# at this point we register location handlers for generating
# EMANE location events
if self.genlocationevents():
iface.poshook = emane_net.setnemposition
iface.setposition()
if stop_emane_on_host:
try:
utils.cmd(kill_emaned)
utils.cmd(kill_transortd)
self.session.distributed.execute(lambda x: x.remote_cmd(kill_emaned))
self.session.distributed.execute(lambda x: x.remote_cmd(kill_transortd))
except CoreCommandError:
logging.exception("error shutting down emane daemons")
def installnetifs(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:
def deinstall_ifaces(self) -> None:
"""
Uninstall TUN/TAP virtual interfaces.
"""
for key in sorted(self._emane_nets.keys()):
emane_node = self._emane_nets[key]
emane_node.deinstallnetifs()
for key in sorted(self._emane_nets):
emane_net = self._emane_nets[key]
for iface in emane_net.get_ifaces():
if iface.is_virtual():
iface.shutdown()
iface.poshook = None
def doeventmonitor(self) -> bool:
"""
@ -717,7 +643,6 @@ class EmaneManager(ModelManager):
logging.info("emane start event monitor")
if not self.doeventmonitor():
return
if self.service is None:
logging.error(
"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.
"""
# convert nemid to node number
_emanenode, netif = self.nemlookup(nemid)
if netif is None:
iface = self.get_iface(nemid)
if iface is None:
logging.info("location event for unknown NEM %s", nemid)
return False
n = netif.node.id
n = iface.node.id
# convert from lat/long/alt to x,y,z coordinates
x, y, z = self.session.location.getxyz(lat, lon, alt)
x = int(x)
@ -868,18 +793,33 @@ class EmaneManager(ModelManager):
result = False
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:
"""
Global EMANE configuration options.
"""
name = "emane"
bitmap = None
name: str = "emane"
bitmap: Optional[str] = None
def __init__(self, session: "Session") -> None:
self.session = session
self.core_config = [
self.session: "Session" = session
self.core_config: List[Configuration] = [
Configuration(
_id="platform_id_start",
_type=ConfigDataTypes.INT32,

View file

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

View file

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

View file

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

View file

@ -6,22 +6,25 @@ share the same MAC+PHY model.
import logging
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.enumerations import (
EventTypes,
LinkTypes,
MessageFlags,
NodeTypes,
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
if TYPE_CHECKING:
from core.emane.emanemodel import EmaneModel
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]
try:
@ -30,6 +33,7 @@ except ImportError:
try:
from emanesh.events import LocationEvent
except ImportError:
LocationEvent = None
logging.debug("compatible emane python bindings not installed")
@ -40,67 +44,63 @@ class EmaneNet(CoreNetworkBase):
Emane controller object that exists in a session.
"""
apitype = NodeTypes.EMANE
linktype = LinkTypes.WIRED
type = "wlan"
is_emane = True
apitype: NodeTypes = NodeTypes.EMANE
linktype: LinkTypes = LinkTypes.WIRED
type: str = "wlan"
has_custom_iface: bool = True
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
start: bool = True,
server: DistributedServer = None,
) -> None:
super().__init__(session, _id, name, start, server)
self.conf = ""
self.nemidmap = {}
self.model = None
self.mobility = None
super().__init__(session, _id, name, server)
self.conf: str = ""
self.model: "OptionalEmaneModel" = None
self.mobility: Optional[WayPointMobility] = None
def linkconfig(
self,
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
) -> None:
"""
The CommEffect model supports link configuration.
"""
if not self.model:
return
self.model.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2)
self.model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None:
self.conf = conf
def startup(self) -> None:
pass
def shutdown(self) -> None:
pass
def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass
def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
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:
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(
"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:
"""
set the EmaneModel associated with this node
"""
logging.info("adding model: %s", model.name)
if model.config_type == RegisterTlvs.WIRELESS:
# EmaneModel really uses values from ConfigurableManager
# 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.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(
self, netif: CoreInterface
self, iface: CoreInterface
) -> Optional[Tuple[int, float, float, float]]:
"""
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
"""
nemid = self.getnemid(netif)
ifname = netif.localname
if nemid is None:
nem_id = self.session.emane.get_nem_id(iface)
ifname = iface.localname
if nem_id is None:
logging.info("nemid for %s is unknown", ifname)
return
node = netif.node
node = iface.node
x, y, z = node.getposition()
lat, lon, alt = self.session.location.getgeo(x, y, z)
if node.position.alt is not None:
@ -205,32 +132,31 @@ class EmaneNet(CoreNetworkBase):
node.position.set_geo(lon, lat, alt)
# altitude must be an integer or warning is printed
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.
:param netif: interface to set nem position for
:param iface: interface to set nem position for
"""
if self.session.emane.service is None:
logging.info("position service not available")
return
position = self._nem_position(netif)
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event = LocationEvent()
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
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
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
if self.session.emane.service is None:
@ -238,18 +164,21 @@ class EmaneNet(CoreNetworkBase):
return
event = LocationEvent()
for netif in moved_netifs:
position = self._nem_position(netif)
for iface in moved_ifaces:
position = self._nem_position(iface)
if position:
nemid, lon, lat, alt = position
event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
self.session.emane.service.publish(0, event)
def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().all_link_data(flags)
# gather current emane links
nem_ids = set(self.nemidmap.values())
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().links(flags)
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
considered = set()
for link_key in emane_links:
@ -268,3 +197,18 @@ class EmaneNet(CoreNetworkBase):
if link:
links.append(link)
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):
# model name
name = "emane_rfpipe"
name: str = "emane_rfpipe"
# mac configuration
mac_library = "rfpipemaclayer"
mac_xml = "rfpipemaclayer.xml"
mac_library: str = "rfpipemaclayer"
mac_xml: str = "rfpipemaclayer.xml"
@classmethod
def load(cls, emane_prefix: str) -> None:

View file

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

View file

@ -3,12 +3,13 @@ import logging
import os
import signal
import sys
from typing import Mapping, Type
from typing import Dict, List, Type
import core.services
from core import configservices
from core import configservices, utils
from core.configservice.manager import ConfigServiceManager
from core.emulator.session import Session
from core.executables import COMMON_REQUIREMENTS, OVS_REQUIREMENTS, VCMD_REQUIREMENTS
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.
"""
def __init__(self, config: Mapping[str, str] = None) -> None:
def __init__(self, config: Dict[str, str] = None) -> None:
"""
Create a CoreEmu object.
@ -48,27 +49,51 @@ class CoreEmu:
# configuration
if config is None:
config = {}
self.config = config
self.config: Dict[str, str] = config
# session management
self.sessions = {}
self.sessions: Dict[int, Session] = {}
# load services
self.service_errors = []
self.service_errors: List[str] = []
self.load_services()
# config services
self.service_manager = ConfigServiceManager()
self.service_manager: ConfigServiceManager = ConfigServiceManager()
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
self.service_manager.load(config_services_path)
custom_dir = self.config.get("custom_config_services_dir")
if custom_dir:
self.service_manager.load(custom_dir)
# check executables exist on path
self._validate_env()
# catch exit event
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:
"""
Loads default and custom services for use within CORE.
:return: nothing
"""
# load default services
self.service_errors = core.services.load()

View file

@ -1,18 +1,22 @@
"""
CORE data objects.
"""
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple
from dataclasses import dataclass
from typing import List, Tuple
import netaddr
from core import utils
from core.emulator.enumerations import (
EventTypes,
ExceptionLevels,
LinkTypes,
MessageFlags,
NodeTypes,
)
if TYPE_CHECKING:
from core.nodes.base import CoreNode, NodeBase
@dataclass
class ConfigData:
@ -27,7 +31,7 @@ class ConfigData:
possible_values: str = None
groups: str = None
session: int = None
interface_number: int = None
iface_id: int = None
network_id: int = None
opaque: str = None
@ -68,65 +72,218 @@ class FileData:
@dataclass
class NodeData:
message_type: MessageFlags = None
id: int = None
node_type: NodeTypes = None
class NodeOptions:
"""
Options for creating and updating nodes within core.
"""
name: str = None
ip_address: str = None
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
model: Optional[str] = "PC"
canvas: int = None
network_id: int = None
services: List[str] = None
latitude: float = None
longitude: float = None
altitude: float = 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
@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
class LinkData:
"""
Represents all data associated with a link.
"""
message_type: MessageFlags = None
type: LinkTypes = LinkTypes.WIRED
label: str = None
node1_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
key: int = None
interface1_id: int = None
interface1_name: str = None
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
iface1: InterfaceData = None
iface2: InterfaceData = None
options: LinkOptions = LinkOptions()
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 host: host to connect to
"""
self.name = name
self.host = host
self.conn = Connection(host, user="root")
self.lock = threading.Lock()
self.name: str = name
self.host: str = host
self.conn: Connection = Connection(host, user="root")
self.lock: threading.Lock = threading.Lock()
def remote_cmd(
self, cmd: str, env: Dict[str, str] = None, cwd: str = None, wait: bool = True
@ -117,10 +117,10 @@ class DistributedController:
:param session: session
"""
self.session = session
self.servers = OrderedDict()
self.tunnels = {}
self.address = self.session.options.get_config(
self.session: "Session" = session
self.servers: Dict[str, DistributedServer] = OrderedDict()
self.tunnels: Dict[int, Tuple[GreTap, GreTap]] = {}
self.address: str = self.session.options.get_config(
"distributed_address", default=None
)
@ -178,13 +178,10 @@ class DistributedController:
"""
for node_id in self.session.nodes:
node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork):
continue
if isinstance(node, CtrlNet) and node.serverintf is not None:
continue
for name in self.servers:
server = self.servers[name]
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.
:param node: node to create gre tunnel for
:param server: server to create
tunnel for
@ -212,7 +208,7 @@ class DistributedController:
"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.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
logging.info(
@ -221,37 +217,27 @@ class DistributedController:
remote_tap = GreTap(
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
tunnel = (local_tap, remote_tap)
self.tunnels[key] = 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.
The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet").
:param n1_id: node one id
:param n2_id: node two id
:param node1_id: node one id
:param node2_id: node two id
: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 = (
(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
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.emulator.enumerations import ConfigDataTypes, RegisterTlvs
@ -10,8 +10,8 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
Provides session configuration.
"""
name = "session"
options = [
name: str = "session"
options: List[Configuration] = [
Configuration(
_id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network"
),
@ -56,8 +56,11 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
default=Sdt.DEFAULT_SDT_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:
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
from tkinter import PhotoImage, font, ttk
from tkinter.ttk import Progressbar
from typing import Any, Dict, Optional, Type
import grpc
from core.gui import appconfig, themes
from core.gui.appconfig import GuiConfig
from core.gui.coreclient import CoreClient
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.images import ImageEnum, Images
from core.gui.menubar import Menubar
from core.gui.nodeutils import NodeUtils
from core.gui.statusbar import StatusBar
from core.gui.themes import PADY
from core.gui.toolbar import Toolbar
WIDTH = 1000
HEIGHT = 800
WIDTH: int = 1000
HEIGHT: int = 800
class Application(ttk.Frame):
@ -27,25 +32,28 @@ class Application(ttk.Frame):
NodeUtils.setup()
# widgets
self.menubar = None
self.toolbar = None
self.right_frame = None
self.canvas = None
self.statusbar = None
self.progress = None
self.menubar: Optional[Menubar] = None
self.toolbar: Optional[Toolbar] = None
self.right_frame: Optional[ttk.Frame] = None
self.canvas: Optional[CanvasGraph] = None
self.statusbar: Optional[StatusBar] = 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
self.fonts_size = None
self.icon_text_font = None
self.edge_font = None
self.fonts_size: Dict[str, int] = {}
self.icon_text_font: Optional[font.Font] = None
self.edge_font: Optional[font.Font] = None
# setup
self.guiconfig = appconfig.read()
self.app_scale = self.guiconfig.scale
self.guiconfig: GuiConfig = appconfig.read()
self.app_scale: float = self.guiconfig.scale
self.setup_scaling()
self.style = ttk.Style()
self.style: ttk.Style = ttk.Style()
self.setup_theme()
self.core = CoreClient(self, proxy)
self.core: CoreClient = CoreClient(self, proxy)
self.setup_app()
self.draw()
self.core.setup()
@ -111,16 +119,27 @@ class Application(ttk.Frame):
self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew")
self.draw_canvas()
self.draw_infobar()
self.draw_status()
self.progress = Progressbar(self.right_frame, mode="indeterminate")
self.menubar = Menubar(self)
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:
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(sticky="nsew", pady=1)
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew")
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
@ -134,7 +153,31 @@ class Application(ttk.Frame):
def draw_status(self) -> None:
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:
logging.exception("app grpc exception", exc_info=e)

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@ check engine light
"""
import tkinter as tk
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.themes import PADX, PADY
from core.gui.widgets import CodeText
@ -15,14 +15,14 @@ if TYPE_CHECKING:
class AlertsDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Alerts")
self.tree = None
self.codetext = None
self.alarm_map = {}
self.tree: Optional[ttk.Treeview] = None
self.codetext: Optional[CodeText] = None
self.alarm_map: Dict[int, ExceptionEvent] = {}
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, 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.grid(row=0, column=1, sticky="ew")
def reset_alerts(self):
def reset_alerts(self) -> None:
self.codetext.text.delete("1.0", tk.END)
for item in self.tree.get_children():
self.tree.delete(item)
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]
alarm = self.alarm_map[current]
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.dialogs.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
from core.gui.app import Application
PIXEL_SCALE = 100
PIXEL_SCALE: int = 100
class SizeAndScaleDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
"""
create an instance for size and scale object
"""
super().__init__(app, "Canvas Size and Scale")
self.canvas = self.app.canvas
self.section_font = font.Font(weight="bold")
self.canvas: CanvasGraph = self.app.canvas
self.section_font: font.Font = font.Font(weight="bold")
width, height = self.canvas.current_dimensions
self.pixel_width = tk.IntVar(value=width)
self.pixel_height = tk.IntVar(value=height)
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.location
self.x = tk.DoubleVar(value=location.x)
self.y = tk.DoubleVar(value=location.y)
self.lat = tk.DoubleVar(value=location.lat)
self.lon = tk.DoubleVar(value=location.lon)
self.alt = tk.DoubleVar(value=location.alt)
self.scale = tk.DoubleVar(value=location.scale)
self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale)
self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale)
self.save_default = tk.BooleanVar(value=False)
self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
self.lon: tk.DoubleVar = tk.DoubleVar(value=location.lon)
self.alt: tk.DoubleVar = tk.DoubleVar(value=location.alt)
self.scale: tk.DoubleVar = tk.DoubleVar(value=location.scale)
self.meters_width: tk.IntVar = tk.IntVar(
value=width / PIXEL_SCALE * location.scale
)
self.meters_height: tk.IntVar = tk.IntVar(
value=height / PIXEL_SCALE * location.scale
)
self.save_default: tk.BooleanVar = tk.BooleanVar(value=False)
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.draw_size()
self.draw_scale()
@ -47,7 +52,7 @@ class SizeAndScaleDialog(Dialog):
self.draw_spacer()
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.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
@ -61,10 +66,12 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
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.grid(row=0, column=2, sticky="w", padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w")
@ -75,16 +82,20 @@ class SizeAndScaleDialog(Dialog):
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = 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)
label = ttk.Label(frame, text="x Height")
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)
label = ttk.Label(frame, text="Meters")
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.grid(sticky="ew")
label_frame.columnconfigure(0, weight=1)
@ -96,10 +107,11 @@ class SizeAndScaleDialog(Dialog):
label.grid(row=0, column=0, sticky="w", padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w")
def draw_reference_point(self):
def draw_reference_point(self) -> None:
label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD
)
@ -150,13 +162,13 @@ class SizeAndScaleDialog(Dialog):
entry = validation.FloatEntry(frame, textvariable=self.alt)
entry.grid(row=0, column=5, sticky="ew")
def draw_save_as_default(self):
def draw_save_as_default(self) -> None:
button = ttk.Checkbutton(
self.top, text="Save as default?", variable=self.save_default
)
button.grid(sticky="w", pady=PADY)
def draw_buttons(self):
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
@ -168,7 +180,14 @@ class SizeAndScaleDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
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()
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:

View file

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

View file

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

View file

@ -4,10 +4,11 @@ Service configuration dialog
import logging
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, Dict, List, Optional, Set
import grpc
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.services_pb2 import ServiceValidationMode
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
@ -16,6 +17,7 @@ from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
class ConfigServiceConfigDialog(Dialog):
@ -26,56 +28,53 @@ class ConfigServiceConfigDialog(Dialog):
service_name: str,
canvas_node: "CanvasNode",
node_id: int,
):
) -> None:
title = f"{service_name} Config Service"
super().__init__(app, title, master=master)
self.core = app.core
self.canvas_node = canvas_node
self.node_id = node_id
self.service_name = service_name
self.radiovar = tk.IntVar()
self.core: "CoreClient" = app.core
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = node_id
self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(2)
self.directories = []
self.templates = []
self.dependencies = []
self.executables = []
self.startup_commands = []
self.validation_commands = []
self.shutdown_commands = []
self.default_startup = []
self.default_validate = []
self.default_shutdown = []
self.validation_mode = None
self.validation_time = None
self.validation_period = tk.StringVar()
self.modes = []
self.mode_configs = {}
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.directories: List[str] = []
self.templates: List[str] = []
self.dependencies: List[str] = []
self.executables: List[str] = []
self.startup_commands: List[str] = []
self.validation_commands: List[str] = []
self.shutdown_commands: List[str] = []
self.default_startup: List[str] = []
self.default_validate: List[str] = []
self.default_shutdown: List[str] = []
self.validation_mode: Optional[ServiceValidationMode] = None
self.validation_time: Optional[int] = None
self.validation_period: tk.StringVar = tk.StringVar()
self.modes: List[str] = []
self.mode_configs: Dict[str, str] = {}
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()
if not self.has_error:
self.draw()
def load(self):
def load(self) -> None:
try:
self.core.create_nodes_and_links()
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.has_error = True
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
@ -130,7 +129,7 @@ class ConfigServiceConfigDialog(Dialog):
self.draw_tab_validation()
self.draw_buttons()
def draw_tab_files(self):
def draw_tab_files(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
@ -174,7 +173,7 @@ class ConfigServiceConfigDialog(Dialog):
)
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.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
@ -198,7 +197,7 @@ class ConfigServiceConfigDialog(Dialog):
self.config_frame.grid(sticky="nsew", pady=PADY)
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.grid(sticky="nsew")
tab.columnconfigure(0, weight=1)
@ -239,7 +238,7 @@ class ConfigServiceConfigDialog(Dialog):
elif i == 2:
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.grid(sticky="ew")
tab.columnconfigure(0, weight=1)
@ -298,7 +297,7 @@ class ConfigServiceConfigDialog(Dialog):
for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self):
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(4):
@ -312,7 +311,7 @@ class ConfigServiceConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
def click_apply(self):
def click_apply(self) -> None:
current_listbox = self.master.current.listbox
if not self.is_custom():
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")
self.destroy()
def handle_template_changed(self, event: tk.Event):
def handle_template_changed(self, event: tk.Event) -> None:
template = self.templates_combobox.get()
self.template_text.text.delete(1.0, "end")
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()
config = self.mode_configs[mode]
logging.info("mode config: %s", 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
template = self.templates_combobox.get()
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
@ -353,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog):
else:
self.modified_files.discard(template)
def is_custom(self):
def is_custom(self) -> bool:
has_custom_templates = len(self.modified_files) > 0
has_custom_config = False
if self.config_frame:
@ -361,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog):
has_custom_config = self.default_config != current
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)
logging.info(
"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)
self.config_frame.set_values(self.default_config)
def click_copy(self):
def click_copy(self) -> None:
pass
def append_commands(
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
):
) -> None:
for cmd in to_add:
commands.append(cmd)
listbox.insert(tk.END, cmd)

View file

@ -4,81 +4,58 @@ copy service config dialog
import tkinter as tk
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.themes import FRAME_PAD, PADX
from core.gui.widgets import CodeText
from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
class CopyServiceConfigDialog(Dialog):
def __init__(self, master: tk.BaseWidget, app: "Application", node_id: int):
super().__init__(app, f"Copy services to node {node_id}", master=master)
self.parent = master
self.node_id = node_id
self.service_configs = app.core.service_configs
self.file_configs = app.core.file_configs
self.tree = None
def __init__(
self,
app: "Application",
dialog: "ServiceConfigDialog",
name: str,
service: str,
file_name: str,
) -> 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()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.tree = ttk.Treeview(self.top)
self.tree.grid(row=0, column=0, sticky="ew", padx=PADX)
self.tree["columns"] = ()
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
cmdup = config.startup[:]
cmddown = config.shutdown[:]
cmdval = config.validate[:]
self.tree.insert(
serviceid,
"end",
text=f"cmdup=({str(cmdup)[1:-1]})",
tags=("cmd", "up"),
)
self.tree.insert(
serviceid,
"end",
text=f"cmddown=({str(cmddown)[1:-1]})",
tags=("cmd", "down"),
)
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")
self.top.rowconfigure(1, weight=1)
label = ttk.Label(
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
)
label.grid(sticky="ew", pady=PADY)
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
self.listbox = listbox_scroll.listbox
for canvas_node in self.app.canvas.nodes.values():
file_configs = canvas_node.service_file_configs.get(self.service)
if not file_configs:
continue
data = file_configs.get(self.file_name)
if not data:
continue
name = canvas_node.core_node.name
self.nodes[name] = canvas_node.id
self.listbox.insert(tk.END, name)
frame = ttk.Frame(self.top)
frame.grid(row=1, column=0)
frame.grid(sticky="ew")
for i in range(3):
frame.columnconfigure(i, weight=1)
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.grid(row=0, column=1, sticky="ew", padx=PADX)
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):
selected = self.tree.selection()
if selected:
item = self.tree.item(selected[0])
if "file" in item["tags"]:
filename = item["text"]
nid, service = self.get_node_service(selected)
data = self.file_configs[nid][service][filename]
if service == self.parent.service_name:
self.parent.temp_service_files[filename] = data
self.parent.modified_files.add(filename)
if self.parent.filename_combobox.get() == filename:
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,
)
def click_copy(self) -> None:
selection = self.listbox.curselection()
if not selection:
return
name = self.listbox.get(selection)
canvas_node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id]
data = canvas_node.service_file_configs[self.service][self.file_name]
self.dialog.temp_service_files[self.file_name] = data
self.dialog.modified_files.add(self.file_name)
self.dialog.service_file_data.text.delete(1.0, tk.END)
self.dialog.service_file_data.text.insert(tk.END, data)
self.destroy()
def click_view(self):
selected = self.tree.selection()
data = ""
if selected:
item = self.tree.item(selected[0])
if "file" in item["tags"]:
nid, service = self.get_node_service(selected)
data = self.file_configs[nid][service][item["text"]]
dialog = ViewConfigDialog(
self, self.app, nid, data, item["text"].split("/")[-1]
)
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
def click_view(self) -> None:
selection = self.listbox.curselection()
if not selection:
return
name = self.listbox.get(selection)
canvas_node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id]
data = canvas_node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog(
self.app, self, name, self.service, self.file_name, data
)
dialog.show()
class ViewConfigDialog(Dialog):
def __init__(
self,
master: tk.BaseWidget,
app: "Application",
node_id: int,
master: tk.BaseWidget,
name: str,
service: str,
file_name: str,
data: str,
filename: str = None,
):
super().__init__(app, f"n{node_id} config data", master=master)
) -> None:
title = f"{name} Service({service}) File({file_name})"
super().__init__(app, title, master=master)
self.data = data
self.service_data = None
self.filepath = tk.StringVar(value=f"/tmp/services.tmp-n{node_id}-{filename}")
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
frame = ttk.Frame(self.top, padding=FRAME_PAD)
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.top.rowconfigure(0, weight=1)
self.service_data = CodeText(self.top)
self.service_data.grid(row=1, column=0, sticky="nsew")
self.service_data.text.insert("end", self.data)
self.service_data.text.config(state="disabled")
self.service_data.grid(sticky="nsew", pady=PADY)
self.service_data.text.insert(tk.END, self.data)
self.service_data.text.config(state=tk.DISABLED)
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
from pathlib import Path
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.appconfig import ICONS_PATH, CustomNode
@ -19,15 +21,15 @@ if TYPE_CHECKING:
class ServicesSelectDialog(Dialog):
def __init__(
self, master: tk.BaseWidget, app: "Application", current_services: Set[str]
):
) -> None:
super().__init__(app, "Node Services", master=master)
self.groups = None
self.services = None
self.current = None
self.current_services = set(current_services)
self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None
self.current_services: Set[str] = current_services
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
@ -77,7 +79,7 @@ class ServicesSelectDialog(Dialog):
# trigger group change
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()
if selection:
index = selection[0]
@ -87,7 +89,7 @@ class ServicesSelectDialog(Dialog):
checked = name in self.current_services
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:
self.current_services.add(name)
elif not var.get() and name in self.current_services:
@ -96,34 +98,34 @@ class ServicesSelectDialog(Dialog):
for name in sorted(self.current_services):
self.current.listbox.insert(tk.END, name)
def click_cancel(self):
def click_cancel(self) -> None:
self.current_services = None
self.destroy()
class CustomNodesDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Custom Nodes")
self.edit_button = None
self.delete_button = None
self.nodes_list = None
self.name = tk.StringVar()
self.image_button = None
self.image = None
self.image_file = None
self.services = set()
self.selected = None
self.selected_index = None
self.edit_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.nodes_list: Optional[ListboxScroll] = None
self.name: tk.StringVar = tk.StringVar()
self.image_button: Optional[ttk.Button] = None
self.image: Optional[PhotoImage] = None
self.image_file: Optional[str] = None
self.services: Set[str] = set()
self.selected: Optional[str] = None
self.selected_index: Optional[int] = None
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.draw_node_config()
self.draw_node_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.grid(sticky="nsew", pady=PADY)
frame.columnconfigure(0, weight=1)
@ -147,7 +149,7 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew")
def draw_node_buttons(self):
def draw_node_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
for i in range(3):
@ -166,7 +168,7 @@ class CustomNodesDialog(Dialog):
)
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.grid(sticky="ew")
for i in range(2):
@ -178,14 +180,14 @@ class CustomNodesDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def reset_values(self):
def reset_values(self) -> None:
self.name.set("")
self.image = None
self.image_file = None
self.services = set()
self.image_button.config(image="")
def click_icon(self):
def click_icon(self) -> None:
file_path = image_chooser(self, ICONS_PATH)
if file_path:
image = Images.create(file_path, nodeutils.ICON_SIZE)
@ -193,24 +195,26 @@ class CustomNodesDialog(Dialog):
self.image_file = file_path
self.image_button.config(image=self.image)
def click_services(self):
def click_services(self) -> None:
dialog = ServicesSelectDialog(self, self.app, self.services)
dialog.show()
if dialog.current_services is not None:
self.services.clear()
self.services.update(dialog.current_services)
def click_save(self):
def click_save(self) -> None:
self.app.guiconfig.nodes.clear()
for name in self.app.core.custom_nodes:
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)
logging.info("saving custom nodes: %s", self.app.guiconfig.nodes)
self.app.save_config()
self.destroy()
def click_create(self):
def click_create(self) -> None:
name = self.name.get()
if name not in self.app.core.custom_nodes:
image_file = Path(self.image_file).stem
@ -226,7 +230,7 @@ class CustomNodesDialog(Dialog):
self.nodes_list.listbox.insert(tk.END, name)
self.reset_values()
def click_edit(self):
def click_edit(self) -> None:
name = self.name.get()
if 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.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:
self.nodes_list.listbox.delete(self.selected_index)
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.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()
if selection:
self.selected_index = selection[0]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
@ -12,15 +12,15 @@ if TYPE_CHECKING:
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)
self.name = tk.StringVar()
self.codetext = None
self.hook = core_pb2.Hook()
self.state = tk.StringVar()
self.name: tk.StringVar = tk.StringVar()
self.codetext: Optional[CodeText] = None
self.hook: core_pb2.Hook = core_pb2.Hook()
self.state: tk.StringVar = tk.StringVar()
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, 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.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()
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.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END)
@ -78,7 +78,7 @@ class HookDialog(Dialog):
state_name = core_pb2.SessionState.Enum.Name(hook.state)
self.state.set(state_name)
def save(self):
def save(self) -> None:
data = self.codetext.text.get("1.0", tk.END).strip()
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
self.hook.file = self.name.get()
@ -88,15 +88,15 @@ class HookDialog(Dialog):
class HooksDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Hooks")
self.listbox = None
self.edit_button = None
self.delete_button = None
self.selected = None
self.listbox: Optional[tk.Listbox] = None
self.edit_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.selected: Optional[str] = None
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(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.grid(row=0, column=3, sticky="ew")
def click_create(self):
def click_create(self) -> None:
dialog = HookDialog(self, self.app)
dialog.show()
hook = dialog.hook
@ -132,19 +132,19 @@ class HooksDialog(Dialog):
self.app.core.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file)
def click_edit(self):
def click_edit(self) -> None:
hook = self.app.core.hooks[self.selected]
dialog = HookDialog(self, self.app)
dialog.set(hook)
dialog.show()
def click_delete(self):
def click_delete(self) -> None:
del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR)
self.edit_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():
index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index)

View file

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

View file

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

View file

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

View file

@ -2,10 +2,12 @@
mobility configuration
"""
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, Optional
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.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -16,23 +18,24 @@ if TYPE_CHECKING:
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")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
self.has_error = False
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None
self.has_error: bool = False
try:
self.config = self.canvas_node.mobility_config
if not self.config:
self.config = self.app.core.get_mobility_config(self.node.id)
config = self.canvas_node.mobility_config
if not config:
config = self.app.core.get_mobility_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.draw()
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Mobility Config Error", e)
self.has_error = True
self.has_error: bool = True
self.destroy()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
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.draw_apply_buttons()
def draw_apply_buttons(self):
def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
for i in range(2):
@ -52,7 +55,7 @@ class MobilityConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
def click_apply(self):
def click_apply(self) -> None:
self.config_frame.parse_config()
self.canvas_node.mobility_config = self.config
self.destroy()

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ core node services
"""
import tkinter as tk
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.serviceconfig import ServiceConfigDialog
@ -16,19 +16,19 @@ if TYPE_CHECKING:
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"
super().__init__(app, title)
self.canvas_node = canvas_node
self.node_id = canvas_node.core_node.id
self.groups = None
self.services = None
self.current = None
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = canvas_node.core_node.id
self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services)
self.current_services = services
self.current_services: Set[str] = services
self.draw()
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
@ -82,9 +82,9 @@ class NodeServiceDialog(Dialog):
button.grid(row=0, column=3, sticky="ew")
# 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()
if selection:
index = selection[0]
@ -94,7 +94,7 @@ class NodeServiceDialog(Dialog):
checked = name in self.current_services
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:
self.current_services.add(name)
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.canvas_node.core_node.services[:] = self.current_services
def click_configure(self):
def click_configure(self) -> None:
current_selection = self.current.listbox.curselection()
if len(current_selection):
dialog = ServiceConfigDialog(
@ -127,12 +127,12 @@ class NodeServiceDialog(Dialog):
"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.services[:] = self.current_services
self.destroy()
def click_remove(self):
def click_remove(self) -> None:
cur = self.current.listbox.curselection()
if cur:
service = self.current.listbox.get(cur[0])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,11 @@ throughput dialog
"""
import tkinter as tk
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.dialog import Dialog
from core.gui.graph.graph import CanvasGraph
from core.gui.themes import FRAME_PAD, PADX, PADY
if TYPE_CHECKING:
@ -14,21 +15,23 @@ if TYPE_CHECKING:
class ThroughputDialog(Dialog):
def __init__(self, app: "Application"):
def __init__(self, app: "Application") -> None:
super().__init__(app, "Throughput Config")
self.canvas = app.canvas
self.show_throughput = tk.IntVar(value=1)
self.exponential_weight = tk.IntVar(value=1)
self.transmission = tk.IntVar(value=1)
self.reception = tk.IntVar(value=1)
self.threshold = tk.DoubleVar(value=self.canvas.throughput_threshold)
self.width = tk.IntVar(value=self.canvas.throughput_width)
self.color = self.canvas.throughput_color
self.color_button = None
self.canvas: CanvasGraph = app.canvas
self.show_throughput: tk.IntVar = tk.IntVar(value=1)
self.exponential_weight: tk.IntVar = tk.IntVar(value=1)
self.transmission: tk.IntVar = tk.IntVar(value=1)
self.reception: tk.IntVar = tk.IntVar(value=1)
self.threshold: tk.DoubleVar = tk.DoubleVar(
value=self.canvas.throughput_threshold
)
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.draw()
def draw(self):
def draw(self) -> None:
button = ttk.Checkbutton(
self.top,
variable=self.show_throughput,
@ -97,12 +100,12 @@ class ThroughputDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
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)
self.color = color_picker.askcolor()
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_width = self.width.get()
self.canvas.throughput_color = self.color

View file

@ -1,8 +1,10 @@
from tkinter import ttk
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, Optional
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.themes import PADX, PADY
from core.gui.widgets import ConfigFrame
@ -10,34 +12,36 @@ from core.gui.widgets import ConfigFrame
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
from core.gui.graph.graph import CanvasGraph
RANGE_COLOR = "#009933"
RANGE_WIDTH = 3
RANGE_COLOR: str = "#009933"
RANGE_WIDTH: int = 3
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")
self.canvas_node = canvas_node
self.node = canvas_node.core_node
self.config_frame = None
self.range_entry = None
self.has_error = False
self.canvas = app.canvas
self.ranges = {}
self.positive_int = self.app.master.register(self.validate_and_update)
self.canvas: "CanvasGraph" = app.canvas
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
self.config_frame: Optional[ConfigFrame] = None
self.range_entry: Optional[ttk.Entry] = None
self.has_error: bool = False
self.ranges: Dict[int, int] = {}
self.positive_int: int = self.app.master.register(self.validate_and_update)
try:
self.config = self.canvas_node.wlan_config
if not self.config:
self.config = self.app.core.get_wlan_config(self.node.id)
config = self.canvas_node.wlan_config
if not config:
config = self.app.core.get_wlan_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
self.init_draw_range()
self.draw()
except grpc.RpcError as e:
self.app.show_grpc_exception("WLAN Config Error", e)
self.has_error = True
self.has_error: bool = True
self.destroy()
def init_draw_range(self):
def init_draw_range(self) -> None:
if self.canvas_node.id in self.canvas.wireless_network:
for cid in self.canvas.wireless_network[self.canvas_node.id]:
x, y = self.canvas.coords(cid)
@ -46,7 +50,7 @@ class WlanConfigDialog(Dialog):
)
self.ranges[cid] = range_id
def draw(self):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
@ -55,7 +59,7 @@ class WlanConfigDialog(Dialog):
self.draw_apply_buttons()
self.top.bind("<Destroy>", self.remove_ranges)
def draw_apply_buttons(self):
def draw_apply_buttons(self) -> None:
"""
create node configuration options
"""
@ -75,7 +79,7 @@ class WlanConfigDialog(Dialog):
button = ttk.Button(frame, text="Cancel", command=self.destroy)
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
"""
@ -87,7 +91,7 @@ class WlanConfigDialog(Dialog):
self.remove_ranges()
self.destroy()
def remove_ranges(self, event=None):
def remove_ranges(self, event=None) -> None:
for cid in self.canvas.find_withtag("range"):
self.canvas.delete(cid)
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 math
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.core_pb2 import Interface, Link
from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
TEXT_DISTANCE = 0.30
EDGE_WIDTH = 3
EDGE_COLOR = "#ff0000"
WIRELESS_WIDTH = 1.5
WIRELESS_COLOR = "#009933"
ARC_DISTANCE = 50
TEXT_DISTANCE: float = 0.30
EDGE_WIDTH: int = 3
EDGE_COLOR: str = "#ff0000"
WIRELESS_WIDTH: float = 3
WIRELESS_COLOR: str = "#009933"
ARC_DISTANCE: int = 50
def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]:
@ -57,20 +60,20 @@ def arc_edges(edges) -> None:
class Edge:
tag = tags.EDGE
tag: str = tags.EDGE
def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None:
self.canvas = canvas
self.id = None
self.src = src
self.dst = dst
self.arc = 0
self.token = None
self.src_label = None
self.middle_label = None
self.dst_label = None
self.color = EDGE_COLOR
self.width = EDGE_WIDTH
self.id: Optional[int] = None
self.src: int = src
self.dst: int = dst
self.arc: int = 0
self.token: Optional[Tuple[int, ...]] = None
self.src_label: Optional[int] = None
self.middle_label: Optional[int] = None
self.dst_label: Optional[int] = None
self.color: str = EDGE_COLOR
self.width: int = EDGE_WIDTH
@classmethod
def create_token(cls, src: int, dst: int) -> Tuple[int, ...]:
@ -120,7 +123,7 @@ class Edge:
fill=self.color,
)
def redraw(self):
def redraw(self) -> None:
self.canvas.itemconfig(self.id, width=self.scaled_width(), fill=self.color)
src_x, src_y, _, _, _, _ = self.canvas.coords(self.id)
src_pos = src_x, src_y
@ -139,11 +142,16 @@ class Edge:
font=self.canvas.app.edge_font,
text=text,
tags=tags.LINK_LABEL,
justify=tk.CENTER,
state=self.canvas.show_link_labels.state(),
)
else:
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]]:
src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id)
v1 = dst_x - src_x
@ -215,11 +223,10 @@ class Edge:
logging.debug("deleting canvas edge, id: %s", self.id)
self.canvas.delete(self.id)
self.canvas.delete(self.src_label)
self.canvas.delete(self.middle_label)
self.canvas.delete(self.dst_label)
self.clear_middle_label()
self.id = None
self.src_label = None
self.middle_label = None
self.dst_label = None
@ -233,14 +240,28 @@ class CanvasWirelessEdge(Edge):
dst: int,
src_pos: Tuple[float, float],
dst_pos: Tuple[float, float],
token: Tuple[Any, ...],
token: Tuple[int, ...],
link: Link,
) -> None:
logging.debug("drawing wireless link from node %s to node %s", src, dst)
super().__init__(canvas, src, dst)
self.token = token
self.width = WIRELESS_WIDTH
self.color = WIRELESS_COLOR
self.link: Link = link
self.token: Tuple[int, ...] = token
self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR
self.color: str = color
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):
@ -259,55 +280,57 @@ class CanvasEdge(Edge):
Create an instance of canvas edge object
"""
super().__init__(canvas, src)
self.src_interface = None
self.dst_interface = None
self.text_src = None
self.text_dst = None
self.link = None
self.asymmetric_link = None
self.throughput = None
self.src_iface: Optional[Interface] = None
self.dst_iface: Optional[Interface] = None
self.text_src: Optional[int] = None
self.text_dst: Optional[int] = None
self.link: Optional[Link] = None
self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos)
self.set_binding()
self.context = tk.Menu(self.canvas)
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
def create_context(self):
def create_context(self) -> None:
themes.style_menu(self.context)
self.context.add_command(label="Configure", command=self.click_configure)
self.context.add_command(label="Delete", command=self.click_delete)
def set_binding(self) -> None:
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.draw_labels()
def interface_label(self, interface: core_pb2.Interface) -> str:
def iface_label(self, iface: core_pb2.Interface) -> str:
label = ""
if interface.name and self.canvas.show_interface_names.get():
label = f"{interface.name}"
if interface.ip4 and self.canvas.show_ip4s.get():
if iface.name and self.canvas.show_iface_names.get():
label = f"{iface.name}"
if iface.ip4 and self.canvas.show_ip4s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip4}/{interface.ip4mask}"
if interface.ip6 and self.canvas.show_ip6s.get():
label += f"{iface.ip4}/{iface.ip4_mask}"
if iface.ip6 and self.canvas.show_ip6s.get():
label = f"{label}\n" if label else ""
label += f"{interface.ip6}/{interface.ip6mask}"
label += f"{iface.ip6}/{iface.ip6_mask}"
return label
def create_node_labels(self) -> Tuple[str, str]:
label_one = None
if self.link.HasField("interface_one"):
label_one = self.interface_label(self.link.interface_one)
label_two = None
if self.link.HasField("interface_two"):
label_two = self.interface_label(self.link.interface_two)
return label_one, label_two
label1 = None
if self.link.HasField("iface1"):
label1 = self.iface_label(self.link.iface1)
label2 = None
if self.link.HasField("iface2"):
label2 = self.iface_label(self.link.iface2)
return label1, label2
def draw_labels(self) -> None:
src_text, dst_text = self.create_node_labels()
self.src_label_text(src_text)
self.dst_label_text(dst_text)
self.draw_link_options()
def redraw(self) -> None:
super().redraw()
@ -378,14 +401,38 @@ class CanvasEdge(Edge):
self.middle_label = None
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:
state = tk.DISABLED if self.canvas.core.is_runtime() else tk.NORMAL
self.context.entryconfigure(1, state=state)
self.context.tk_popup(event.x_root, event.y_root)
def click_delete(self):
def click_delete(self) -> None:
self.canvas.delete_edge(self)
def click_configure(self) -> None:
dialog = LinkConfigurationDialog(self.canvas.app, self)
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
from copy import deepcopy
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.graph import tags
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.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, TypeToImage
from core.gui.nodeutils import NodeUtils
from core.gui.nodeutils import NodeDraw, NodeUtils
if TYPE_CHECKING:
from core.gui.app import Application
@ -48,58 +56,59 @@ class ShowVar(BooleanVar):
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")
self.app = app
self.core = core
self.mode = GraphMode.SELECT
self.annotation_type = None
self.selection = {}
self.select_box = None
self.selected = None
self.node_draw = None
self.nodes = {}
self.edges = {}
self.shapes = {}
self.wireless_edges = {}
self.app: "Application" = app
self.core: "CoreClient" = core
self.mode: GraphMode = GraphMode.SELECT
self.annotation_type: Optional[ShapeType] = None
self.selection: Dict[int, int] = {}
self.select_box: Optional[Shape] = None
self.selected: Optional[int] = None
self.node_draw: Optional[NodeDraw] = None
self.nodes: Dict[int, CanvasNode] = {}
self.edges: Dict[int, CanvasEdge] = {}
self.shapes: Dict[int, Shape] = {}
self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {}
# 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.rect = None
self.shape_drawing = False
self.drawing_edge: Optional[CanvasEdge] = None
self.rect: Optional[int] = None
self.shape_drawing: bool = False
width = self.app.guiconfig.preferences.width
height = self.app.guiconfig.preferences.height
self.default_dimensions = (width, height)
self.current_dimensions = self.default_dimensions
self.ratio = 1.0
self.offset = (0, 0)
self.cursor = (0, 0)
self.marker_tool = None
self.to_copy = []
self.default_dimensions: Tuple[int, int] = (width, height)
self.current_dimensions: Tuple[int, int] = self.default_dimensions
self.ratio: float = 1.0
self.offset: Tuple[int, int] = (0, 0)
self.cursor: Tuple[int, int] = (0, 0)
self.to_copy: List[CanvasNode] = []
# background related
self.wallpaper_id = None
self.wallpaper = None
self.wallpaper_drawn = None
self.wallpaper_file = ""
self.scale_option = tk.IntVar(value=1)
self.adjust_to_dim = tk.BooleanVar(value=False)
self.wallpaper_id: Optional[int] = None
self.wallpaper: Optional[Image.Image] = None
self.wallpaper_drawn: Optional[PhotoImage] = None
self.wallpaper_file: str = ""
self.scale_option: tk.IntVar = tk.IntVar(value=1)
self.adjust_to_dim: tk.BooleanVar = tk.BooleanVar(value=False)
# throughput related
self.throughput_threshold = 250.0
self.throughput_width = 10
self.throughput_color = "#FF0000"
self.throughput_threshold: float = 250.0
self.throughput_width: int = 10
self.throughput_color: str = "#FF0000"
# drawing related
self.show_node_labels = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_grid = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True)
self.show_interface_names = BooleanVar(value=False)
self.show_ip4s = BooleanVar(value=True)
self.show_ip6s = BooleanVar(value=True)
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False)
self.show_ip4s: BooleanVar = BooleanVar(value=True)
self.show_ip6s: BooleanVar = BooleanVar(value=True)
# bindings
self.setup_bindings()
@ -108,7 +117,7 @@ class CanvasGraph(tk.Canvas):
self.draw_canvas()
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:
self.delete(self.rect)
if not dimensions:
@ -125,7 +134,7 @@ class CanvasGraph(tk.Canvas):
)
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
client.
@ -136,7 +145,7 @@ class CanvasGraph(tk.Canvas):
self.show_link_labels.set(True)
self.show_grid.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_ip6s.set(True)
@ -157,7 +166,7 @@ class CanvasGraph(tk.Canvas):
self.drawing_edge = None
self.draw_session(session)
def setup_bindings(self):
def setup_bindings(self) -> None:
"""
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("<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_y = (y - self.offset[1]) / self.ratio
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_y = (y * self.ratio) + self.offset[1]
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)
valid_x = x1 <= x <= x2
valid_y = y1 <= y <= y2
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_bottomright = self.inside_canvas(x2, y2)
return valid_topleft and valid_bottomright
def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent):
for interface_throughput in throughputs_event.interface_throughputs:
node_id = interface_throughput.node_id
interface_id = interface_throughput.interface_id
throughput = interface_throughput.throughput
interface_to_edge_id = (node_id, interface_id)
token = self.core.interface_to_edge.get(interface_to_edge_id)
def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None:
for iface_throughput in throughputs_event.iface_throughputs:
node_id = iface_throughput.node_id
iface_id = iface_throughput.iface_id
throughput = iface_throughput.throughput
iface_to_edge_id = (node_id, iface_id)
token = self.core.iface_to_edge.get(iface_to_edge_id)
if not token:
continue
edge = self.edges.get(token)
if edge:
edge.set_throughput(throughput)
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.
"""
@ -223,9 +232,51 @@ class CanvasGraph(tk.Canvas):
self.tag_lower(tags.GRIDLINE)
self.tag_lower(self.rect)
def add_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
) -> None:
def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
token = create_edge_token(src.id, dst.id)
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
token = create_edge_token(src.id, dst.id, network_id)
if token in self.wireless_edges:
@ -233,11 +284,7 @@ class CanvasGraph(tk.Canvas):
return
src_pos = self.coords(src.id)
dst_pos = self.coords(dst.id)
edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token)
if link.label:
edge.middle_label_text(link.label)
if link.color:
edge.color = link.color
edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token, link)
self.wireless_edges[token] = edge
src.wireless_edges.add(edge)
dst.wireless_edges.add(edge)
@ -248,7 +295,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges)
def delete_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
network_id = link.network_id if link.network_id else None
token = create_edge_token(src.id, dst.id, network_id)
@ -263,7 +310,7 @@ class CanvasGraph(tk.Canvas):
arc_edges(common_edges)
def update_wireless_edge(
self, src: CanvasNode, dst: CanvasNode, link: core_pb2.Link
self, src: CanvasNode, dst: CanvasNode, link: Link
) -> None:
if not link.label:
return
@ -275,73 +322,43 @@ class CanvasGraph(tk.Canvas):
edge = self.wireless_edges[token]
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:
logging.error("core node already exists: %s", core_node)
return
logging.debug("adding node %s", core_node)
# if the gui can't find node's image, default to the "edit-node" image
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, x, y, core_node, image)
self.nodes[node.id] = node
self.core.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:
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 not image:
image = self.app.get_icon(ImageEnum.EDITNODE, ICON_SIZE)
x = core_node.position.x
y = core_node.position.y
node = CanvasNode(self.app, x, y, core_node, image)
self.nodes[node.id] = node
self.core.canvas_nodes[core_node.id] = node
self.add_core_node(core_node)
# draw existing links
# draw existing links
for link in session.links:
logging.debug("drawing link: %s", link)
canvas_node_one = self.core.canvas_nodes[link.node_one_id]
node_one = canvas_node_one.core_node
canvas_node_two = self.core.canvas_nodes[link.node_two_id]
node_two = canvas_node_two.core_node
token = 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)
canvas_node1 = self.core.canvas_nodes[link.node1_id]
canvas_node2 = self.core.canvas_nodes[link.node2_id]
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link)
else:
if token not in self.edges:
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)
self.add_wired_edge(canvas_node1, canvas_node2, link)
def stopped_session(self):
def stopped_session(self) -> None:
# clear wireless edges
for edge in self.wireless_edges.values():
edge.delete()
@ -351,11 +368,10 @@ class CanvasGraph(tk.Canvas):
dst_node.wireless_edges.remove(edge)
self.wireless_edges.clear()
# clear all middle edge labels
for edge in self.edges.values():
edge.reset()
# clear throughputs
self.clear_throughputs()
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
"""
@ -383,7 +399,7 @@ class CanvasGraph(tk.Canvas):
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
"""
@ -422,7 +438,7 @@ class CanvasGraph(tk.Canvas):
self.mode = GraphMode.NODE
self.selected = None
def handle_edge_release(self, _event: tk.Event):
def handle_edge_release(self, _event: tk.Event) -> None:
edge = self.drawing_edge
self.drawing_edge = None
@ -432,8 +448,9 @@ class CanvasGraph(tk.Canvas):
# edge dst must be a node
logging.debug("current selected: %s", self.selected)
src_node = self.nodes.get(edge.src)
dst_node = self.nodes.get(self.selected)
if not dst_node:
if not dst_node or not src_node:
edge.delete()
return
@ -448,17 +465,23 @@ class CanvasGraph(tk.Canvas):
edge.delete()
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
edge.complete(self.selected)
self.edges[edge.token] = edge
node_src = self.nodes[edge.src]
node_src.edges.add(edge)
node_dst = self.nodes[edge.dst]
node_dst.edges.add(edge)
self.core.create_link(edge, node_src, node_dst)
src_node.edges.add(edge)
dst_node.edges.add(edge)
self.core.create_link(edge, src_node, dst_node)
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
"""
@ -479,7 +502,7 @@ class CanvasGraph(tk.Canvas):
selection_id = self.selection.pop(object_id)
self.delete(selection_id)
def clear_selection(self):
def clear_selection(self) -> None:
"""
Clear current selection boxes.
"""
@ -487,7 +510,7 @@ class CanvasGraph(tk.Canvas):
self.delete(_id)
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)
if select_id is not None:
self.move(select_id, x_offset, y_offset)
@ -515,14 +538,14 @@ class CanvasGraph(tk.Canvas):
edge.delete()
# update node connected to edge being deleted
other_id = edge.src
other_interface = edge.src_interface
other_iface = edge.src_iface
if edge.src == object_id:
other_id = edge.dst
other_interface = edge.dst_interface
other_iface = edge.dst_iface
other_node = self.nodes[other_id]
other_node.edges.remove(edge)
if other_interface:
del other_node.interfaces[other_interface.id]
if other_iface:
del other_node.ifaces[other_iface.id]
if is_wireless:
other_node.delete_antenna()
@ -535,17 +558,17 @@ class CanvasGraph(tk.Canvas):
self.core.deleted_graph_nodes(nodes)
self.core.deleted_graph_edges(edges)
def delete_edge(self, edge: CanvasEdge):
def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete()
del self.edges[edge.token]
src_node = self.nodes[edge.src]
src_node.edges.discard(edge)
if edge.src_interface:
del src_node.interfaces[edge.src_interface.id]
if edge.src_iface:
del src_node.ifaces[edge.src_iface.id]
dst_node = self.nodes[edge.dst]
dst_node.edges.discard(edge)
if edge.dst_interface:
del dst_node.interfaces[edge.dst_interface.id]
if edge.dst_iface:
del dst_node.ifaces[edge.dst_iface.id]
src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
if src_wireless:
dst_node.delete_antenna()
@ -554,7 +577,7 @@ class CanvasGraph(tk.Canvas):
src_node.delete_antenna()
self.core.deleted_graph_edges([edge])
def zoom(self, event: tk.Event, factor: float = None):
def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor:
factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT
event.x, event.y = self.canvasx(event.x), self.canvasy(event.y)
@ -572,7 +595,7 @@ class CanvasGraph(tk.Canvas):
if self.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
"""
@ -634,7 +657,7 @@ class CanvasGraph(tk.Canvas):
self.select_box = shape
self.clear_selection()
def ctrl_click(self, event: tk.Event):
def ctrl_click(self, event: tk.Event) -> None:
# update cursor location
x, y = self.canvas_xy(event)
if not self.inside_canvas(x, y):
@ -652,7 +675,7 @@ class CanvasGraph(tk.Canvas):
):
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)
if not self.inside_canvas(x, y):
if self.select_box:
@ -705,17 +728,18 @@ class CanvasGraph(tk.Canvas):
if self.select_box and self.mode == GraphMode.SELECT:
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
"""
logging.debug("press delete key")
if not self.app.core.is_runtime():
self.delete_selected_objects()
self.app.default_info()
else:
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)
if selected is not None and selected in self.shapes:
shape = self.shapes[selected]
@ -741,7 +765,7 @@ class CanvasGraph(tk.Canvas):
self.core.canvas_nodes[core_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
"""
@ -757,8 +781,8 @@ class CanvasGraph(tk.Canvas):
return image
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:
x1, y1, x2, y2 = self.bbox(self.rect)
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_drawn = image
def wallpaper_upper_left(self):
def wallpaper_upper_left(self) -> None:
self.delete(self.wallpaper_id)
# create new scaled image, cropped if needed
@ -779,7 +803,7 @@ class CanvasGraph(tk.Canvas):
if image.height > height:
cropy = image.height
cropped = image.crop((0, 0, cropx, cropy))
image = ImageTk.PhotoImage(cropped)
image = PhotoImage(cropped)
# draw on canvas
x1, y1, _, _ = self.bbox(self.rect)
@ -787,7 +811,7 @@ class CanvasGraph(tk.Canvas):
y = (cropy / 2) + y1
self.draw_wallpaper(image, x, y)
def wallpaper_center(self):
def wallpaper_center(self) -> None:
"""
place the image at the center of canvas
"""
@ -807,26 +831,26 @@ class CanvasGraph(tk.Canvas):
x2 = image.width - cropx
y2 = image.height - cropy
cropped = image.crop((x1, y1, x2, y2))
image = ImageTk.PhotoImage(cropped)
image = PhotoImage(cropped)
self.draw_wallpaper(image)
def wallpaper_scaled(self):
def wallpaper_scaled(self) -> None:
"""
scale image based on canvas dimension
"""
self.delete(self.wallpaper_id)
canvas_w, canvas_h = self.width_and_height()
image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS)
image = ImageTk.PhotoImage(image)
image = PhotoImage(image)
self.draw_wallpaper(image)
def resize_to_wallpaper(self):
def resize_to_wallpaper(self) -> None:
self.delete(self.wallpaper_id)
image = ImageTk.PhotoImage(self.wallpaper)
image = PhotoImage(self.wallpaper)
self.redraw_canvas((image.width(), image.height()))
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)
# reset scale and move back to original position
@ -847,7 +871,7 @@ class CanvasGraph(tk.Canvas):
self.draw_grid()
self.app.canvas.show_grid.click_handler()
def redraw_wallpaper(self):
def redraw_wallpaper(self) -> None:
if self.adjust_to_dim.get():
logging.debug("drawing wallpaper to canvas dimensions")
self.resize_to_wallpaper()
@ -868,7 +892,7 @@ class CanvasGraph(tk.Canvas):
for tag in tags.ORGANIZE_TAGS:
self.tag_raise(tag)
def set_wallpaper(self, filename: str):
def set_wallpaper(self, filename: Optional[str]) -> None:
logging.debug("setting wallpaper: %s", filename)
if filename:
img = Image.open(filename)
@ -884,7 +908,7 @@ class CanvasGraph(tk.Canvas):
def is_selection_mode(self) -> bool:
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
"""
@ -898,7 +922,7 @@ class CanvasGraph(tk.Canvas):
self.nodes[dest.id].edges.add(edge)
self.core.create_link(edge, source, dest)
def copy(self):
def copy(self) -> None:
if self.core.is_runtime():
logging.debug("copy is disabled during runtime state")
return
@ -909,7 +933,7 @@ class CanvasGraph(tk.Canvas):
canvas_node = self.nodes[node_id]
self.to_copy.append(canvas_node)
def paste(self):
def paste(self) -> None:
if self.core.is_runtime():
logging.debug("paste is disabled during runtime state")
return
@ -965,26 +989,26 @@ class CanvasGraph(tk.Canvas):
copy_link = copy_edge.link
options = edge.link.options
copy_link.options.CopyFrom(options)
interface_one = None
if copy_link.HasField("interface_one"):
interface_one = copy_link.interface_one.id
interface_two = None
if copy_link.HasField("interface_two"):
interface_two = copy_link.interface_two.id
iface1_id = None
if copy_link.HasField("iface1"):
iface1_id = copy_link.iface1.id
iface2_id = None
if copy_link.HasField("iface2"):
iface2_id = copy_link.iface2.id
if not options.unidirectional:
copy_edge.asymmetric_link = None
else:
asym_interface_one = None
if interface_one:
asym_interface_one = core_pb2.Interface(id=interface_one)
asym_interface_two = None
if interface_two:
asym_interface_two = core_pb2.Interface(id=interface_two)
copy_edge.asymmetric_link = core_pb2.Link(
node_one_id=copy_link.node_two_id,
node_two_id=copy_link.node_one_id,
interface_one=asym_interface_one,
interface_two=asym_interface_two,
asym_iface1 = None
if iface1_id:
asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None
if iface2_id:
asym_iface2 = Interface(id=iface2_id)
copy_edge.asymmetric_link = Link(
node1_id=copy_link.node2_id,
node2_id=copy_link.node1_id,
iface1=asym_iface1,
iface2=asym_iface2,
options=edge.asymmetric_link.options,
)
self.itemconfig(
@ -994,7 +1018,12 @@ class CanvasGraph(tk.Canvas):
)
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():
img = None
if NodeUtils.is_custom(

View file

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

View file

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

View file

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

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