Merge branch 'develop' into enhancement/poetry-invoke
This commit is contained in:
commit
3949bd6d1b
201 changed files with 8902 additions and 8679 deletions
2
.github/workflows/daemon-checks.yml
vendored
2
.github/workflows/daemon-checks.yml
vendored
|
@ -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: |
|
||||
|
|
37
CHANGELOG.md
37
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]]:
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
ifaces = "-i " + " -i ".join(ifnames)
|
||||
%>
|
||||
olsrd ${interfaces}
|
||||
olsrd ${ifaces}
|
||||
|
|
|
@ -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 &
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
135
daemon/core/configservices/securityservices/services.py
Normal file
135
daemon/core/configservices/securityservices/services.py
Normal 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)
|
|
@ -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)
|
|
@ -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"},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -11,6 +11,7 @@ except ImportError:
|
|||
try:
|
||||
from emanesh import manifest
|
||||
except ImportError:
|
||||
manifest = None
|
||||
logging.debug("compatible emane python bindings not installed")
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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__()
|
||||
|
|
16
daemon/core/executables.py
Normal file
16
daemon/core/executables.py
Normal 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]
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
0
daemon/core/gui/frames/__init__.py
Normal file
0
daemon/core/gui/frames/__init__.py
Normal file
36
daemon/core/gui/frames/base.py
Normal file
36
daemon/core/gui/frames/base.py
Normal 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
|
19
daemon/core/gui/frames/default.py
Normal file
19
daemon/core/gui/frames/default.py
Normal 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)
|
113
daemon/core/gui/frames/link.py
Normal file
113
daemon/core/gui/frames/link.py
Normal 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)
|
39
daemon/core/gui/frames/node.py
Normal file
39
daemon/core/gui/frames/node.py
Normal 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)
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue