Merge pull request #507 from coreemu/develop

Develop
This commit is contained in:
bharnden 2020-08-21 21:12:03 -07:00 committed by GitHub
commit 87ca431e73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 2542 additions and 1829 deletions

View file

@ -1,3 +1,30 @@
## 2020-08-21 CORE 7.1.0
* Installation
* added core-python script that gets installed to help globally reference the virtual environment
* gRPC API
* GetSession will now return all configuration information for a session and the file it was opened from, if applicable
* node update events will now include icon information
* fixed issue with getting session throughputs for sessions with a high id
* core-daemon
* \#503 - EMANE networks will now work with mobility again
* \#506 - fixed service dependency resolution issue
* fixed issue sending hooks to core-gui when joining session
* core-pygui
* fixed issues editing hooks
* fixed issue with cpu usage when joining a session
* fixed mac field not being disabled during runtime when configuring a node
* removed unlimited button from link config dialog
* fixed issue with copy/paste links and their options
* fixed issue with adding nodes/links and editing links during runtime
* updated open file dialog in config dialogs to open to ~/.coregui home directory
* fixed issue double clicking sessions dialog in invalid areas
* added display of asymmetric link options on links
* fixed emane config dialog display
* fixed issue saving backgrounds in xml files
* added view toggle for wired/wireless links
* node events will now update icons
## 2020-07-28 CORE 7.0.1
* Bugfixes

View file

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

View file

@ -552,11 +552,12 @@ class CoreGrpcClient:
source: str = None,
) -> core_pb2.EditNodeResponse:
"""
Edit a node, currently only changes position.
Edit a node's icon and/or location, can only use position(x,y) or
geo(lon, lat, alt), not both.
:param session_id: session id
:param node_id: node id
:param position: position to set node to
:param position: x,y location for node
:param icon: path to icon for gui to use for node
:param geo: lon,lat,alt location for node
:param source: application source

View file

@ -32,6 +32,7 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event:
id=node.id,
name=node.name,
model=node.type,
icon=node.icon,
position=position,
geo=geo,
services=services,

View file

@ -8,14 +8,25 @@ 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.api.grpc.common_pb2 import MappedConfig
from core.api.grpc.configservices_pb2 import ConfigServiceConfig
from core.api.grpc.emane_pb2 import GetEmaneModelConfig
from core.api.grpc.services_pb2 import (
NodeServiceConfig,
NodeServiceData,
ServiceConfig,
ServiceDefaults,
)
from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet
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 CoreNode, NodeBase
from core.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import WlanNode
from core.services.coreservices import CoreService
WORKERS = 10
@ -218,7 +229,7 @@ def get_config_options(
"""
results = {}
for configuration in configurable_options.configurations():
value = config[configuration.id]
value = config.get(configuration.id, configuration.default)
config_option = common_pb2.ConfigOption(
label=configuration.label,
name=configuration.id,
@ -536,3 +547,131 @@ def get_nem_id(
message = f"{node.name} interface {iface_id} nem id does not exist"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
return nem_id
def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]:
configs = []
for _id in session.emane.node_configurations:
if _id == -1:
continue
model_configs = session.emane.node_configurations[_id]
for model_name in model_configs:
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, iface_id = parse_emane_model_id(_id)
model_config = GetEmaneModelConfig(
node_id=node_id, model=model_name, iface_id=iface_id, config=config
)
configs.append(model_config)
return configs
def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]:
configs = {}
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != BasicRangeModel.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, BasicRangeModel)
mapped_config = MappedConfig(config=config)
configs[node_id] = mapped_config
return configs
def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]:
configs = {}
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != Ns2ScriptedMobility.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, Ns2ScriptedMobility)
mapped_config = MappedConfig(config=config)
configs[node_id] = mapped_config
return configs
def get_hooks(session: Session) -> List[core_pb2.Hook]:
hooks = []
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)
return hooks
def get_emane_models(session: Session) -> List[str]:
emane_models = []
for model in session.emane.models.keys():
if len(model.split("_")) != 2:
continue
emane_models.append(model)
return emane_models
def get_default_services(session: Session) -> List[ServiceDefaults]:
default_services = []
for name, services in session.services.default_services.items():
default_service = ServiceDefaults(node_type=name, services=services)
default_services.append(default_service)
return default_services
def get_node_service_configs(session: Session) -> List[NodeServiceConfig]:
configs = []
for node_id, service_configs in session.services.custom_services.items():
for name in service_configs:
service = session.services.get_service(node_id, name)
service_proto = get_service_configuration(service)
config = NodeServiceConfig(
node_id=node_id,
service=name,
data=service_proto,
files=service.config_data,
)
configs.append(config)
return configs
def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]:
configs = []
for node in session.nodes.values():
if not isinstance(node, CoreNodeBase):
continue
for name, service in node.config_services.items():
if not service.custom_templates and not service.custom_config:
continue
config_proto = ConfigServiceConfig(
node_id=node.id,
name=name,
templates=service.custom_templates,
config=service.custom_config,
)
configs.append(config_proto)
return configs
def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]:
current_config = session.emane.get_configs()
return get_config_options(current_config, session.emane.emane_config)
def get_mobility_node(
session: Session, node_id: int, context: ServicerContext
) -> Union[WlanNode, EmaneNet]:
try:
return session.get_node(node_id, WlanNode)
except CoreError:
try:
return session.get_node(node_id, EmaneNet)
except CoreError:
context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane")

View file

@ -19,7 +19,6 @@ from core.api.grpc import (
core_pb2_grpc,
grpcutils,
)
from core.api.grpc.common_pb2 import MappedConfig
from core.api.grpc.configservices_pb2 import (
ConfigService,
GetConfigServiceDefaultsRequest,
@ -89,7 +88,6 @@ from core.api.grpc.services_pb2 import (
ServiceAction,
ServiceActionRequest,
ServiceActionResponse,
ServiceDefaults,
SetNodeServiceFileRequest,
SetNodeServiceFileResponse,
SetNodeServiceRequest,
@ -118,12 +116,13 @@ from core.emulator.enumerations import (
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.base import CoreNode, NodeBase
from core.nodes.network import PtpNet, WlanNode
from core.services.coreservices import ServiceManager
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
_MAX_WORKERS = 1000
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -152,7 +151,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def listen(self, address: str) -> None:
logging.info("CORE gRPC API listening on: %s", address)
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS))
core_pb2_grpc.add_CoreApiServicer_to_server(self, self.server)
self.server.add_insecure_port(address)
self.server.start()
@ -558,7 +557,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get session: %s", request)
session = self.get_session(request.session_id, context)
links = []
nodes = []
for _id in session.nodes:
@ -568,9 +566,39 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes.append(node_proto)
node_links = get_links(node)
links.extend(node_links)
default_services = grpcutils.get_default_services(session)
x, y, z = session.location.refxyz
lat, lon, alt = session.location.refgeo
location = core_pb2.SessionLocation(
x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale
)
hooks = grpcutils.get_hooks(session)
emane_models = grpcutils.get_emane_models(session)
emane_config = grpcutils.get_emane_config(session)
emane_model_configs = grpcutils.get_emane_model_configs(session)
wlan_configs = grpcutils.get_wlan_configs(session)
mobility_configs = grpcutils.get_mobility_configs(session)
service_configs = grpcutils.get_node_service_configs(session)
config_service_configs = grpcutils.get_node_config_service_configs(session)
session_proto = core_pb2.Session(
state=session.state.value, nodes=nodes, links=links, dir=session.session_dir
id=session.id,
state=session.state.value,
nodes=nodes,
links=links,
dir=session.session_dir,
user=session.user,
default_services=default_services,
location=location,
hooks=hooks,
emane_models=emane_models,
emane_config=emane_config,
emane_model_configs=emane_model_configs,
wlan_configs=wlan_configs,
service_configs=service_configs,
config_service_configs=config_service_configs,
mobility_configs=mobility_configs,
metadata=session.metadata,
file=session.file_name,
)
return core_pb2.GetSessionResponse(session=session_proto)
@ -652,9 +680,9 @@ 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)
iface_id = int(key[1], base=16)
session_id = int(key[2], base=16)
if session.id != session_id:
iface_id = int(key[1])
session_id = key[2]
if session.short_session_id() != session_id:
continue
iface_throughput = throughputs_event.iface_throughputs.add()
iface_throughput.node_id = node_id
@ -664,8 +692,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
try:
key = key.split(".")
node_id = int(key[1], base=16)
session_id = int(key[2], base=16)
if session.id != session_id:
session_id = key[2]
if session.short_session_id() != session_id:
continue
bridge_throughput = (
throughputs_event.bridge_throughputs.add()
@ -1012,12 +1040,7 @@ 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 file_name, file_data in state_hooks:
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
hooks.append(hook)
hooks = grpcutils.get_hooks(session)
return core_pb2.GetHooksResponse(hooks=hooks)
def AddHook(
@ -1050,19 +1073,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get mobility configs: %s", request)
session = self.get_session(request.session_id, context)
response = GetMobilityConfigsResponse()
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != Ns2ScriptedMobility.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, Ns2ScriptedMobility)
mapped_config = MappedConfig(config=config)
response.configs[node_id].CopyFrom(mapped_config)
return response
configs = grpcutils.get_mobility_configs(session)
return GetMobilityConfigsResponse(configs=configs)
def GetMobilityConfig(
self, request: GetMobilityConfigRequest, context: ServicerContext
@ -1115,7 +1127,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("mobility action: %s", request)
session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, WlanNode)
node = grpcutils.get_mobility_node(session, request.node_id, context)
if not node.mobility:
context.abort(
grpc.StatusCode.NOT_FOUND, f"node({node.name}) does not have mobility"
)
result = True
if request.action == MobilityAction.START:
node.mobility.start()
@ -1157,12 +1173,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get service defaults: %s", request)
session = self.get_session(request.session_id, context)
all_service_defaults = []
for node_type in session.services.default_services:
services = session.services.default_services[node_type]
service_defaults = ServiceDefaults(node_type=node_type, services=services)
all_service_defaults.append(service_defaults)
return GetServiceDefaultsResponse(defaults=all_service_defaults)
defaults = grpcutils.get_default_services(session)
return GetServiceDefaultsResponse(defaults=defaults)
def SetServiceDefaults(
self, request: SetServiceDefaultsRequest, context: ServicerContext
@ -1196,18 +1208,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get node service configs: %s", request)
session = self.get_session(request.session_id, context)
configs = []
for node_id, service_configs in session.services.custom_services.items():
for name in service_configs:
service = session.services.get_service(node_id, name)
service_proto = grpcutils.get_service_configuration(service)
config = GetNodeServiceConfigsResponse.ServiceConfig(
node_id=node_id,
service=name,
data=service_proto,
files=service.config_data,
)
configs.append(config)
configs = grpcutils.get_node_service_configs(session)
return GetNodeServiceConfigsResponse(configs=configs)
def GetNodeService(
@ -1337,19 +1338,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get wlan configs: %s", request)
session = self.get_session(request.session_id, context)
response = GetWlanConfigsResponse()
for node_id in session.mobility.node_configurations:
model_config = session.mobility.node_configurations[node_id]
if node_id == -1:
continue
for model_name in model_config:
if model_name != BasicRangeModel.name:
continue
current_config = session.mobility.get_model_config(node_id, model_name)
config = get_config_options(current_config, BasicRangeModel)
mapped_config = MappedConfig(config=config)
response.configs[node_id].CopyFrom(mapped_config)
return response
configs = grpcutils.get_wlan_configs(session)
return GetWlanConfigsResponse(configs=configs)
def GetWlanConfig(
self, request: GetWlanConfigRequest, context: ServicerContext
@ -1401,8 +1391,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get emane config: %s", request)
session = self.get_session(request.session_id, context)
current_config = session.emane.get_configs()
config = get_config_options(current_config, session.emane.emane_config)
config = grpcutils.get_emane_config(session)
return GetEmaneConfigResponse(config=config)
def SetEmaneConfig(
@ -1433,11 +1422,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get emane models: %s", request)
session = self.get_session(request.session_id, context)
models = []
for model in session.emane.models.keys():
if len(model.split("_")) != 2:
continue
models.append(model)
models = grpcutils.get_emane_models(session)
return GetEmaneModelsResponse(models=models)
def GetEmaneModelConfig(
@ -1491,22 +1476,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
"""
logging.debug("get emane model configs: %s", request)
session = self.get_session(request.session_id, context)
configs = []
for _id in session.emane.node_configurations:
if _id == -1:
continue
model_configs = session.emane.node_configurations[_id]
for model_name in model_configs:
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, iface_id = grpcutils.parse_emane_model_id(_id)
model_config = GetEmaneModelConfigsResponse.ModelConfig(
node_id=node_id, model=model_name, iface_id=iface_id, config=config
)
configs.append(model_config)
configs = grpcutils.get_emane_model_configs(session)
return GetEmaneModelConfigsResponse(configs=configs)
def SaveXml(
@ -1713,21 +1683,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: get node config service configs response
"""
session = self.get_session(request.session_id, context)
configs = []
for node in session.nodes.values():
if not isinstance(node, CoreNodeBase):
continue
for name, service in node.config_services.items():
if not service.custom_templates and not service.custom_config:
continue
config_proto = configservices_pb2.ConfigServiceConfig(
node_id=node.id,
name=name,
templates=service.custom_templates,
config=service.custom_config,
)
configs.append(config_proto)
configs = grpcutils.get_node_config_service_configs(session)
return GetNodeConfigServiceConfigsResponse(configs=configs)
def GetNodeConfigServices(

View file

@ -520,8 +520,8 @@ class CoreLinkTlv(CoreTlv):
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.IFACE1_NAME.value: CoreTlvDataString,
LinkTlvs.IFACE2_NAME.value: CoreTlvDataString,
LinkTlvs.OPAQUE.value: CoreTlvDataString,
}

View file

@ -759,7 +759,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
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),
name=message.get_tlv(LinkTlvs.IFACE1_NAME.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),
@ -768,7 +768,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
)
iface2_data = InterfaceData(
id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
name=message.get_tlv(LinkTlvs.IFACE2_NAME.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),
@ -1903,12 +1903,12 @@ class CoreHandler(socketserver.BaseRequestHandler):
# TODO: send location info
# send hook scripts
for state in sorted(self.session.hooks.keys()):
for state in sorted(self.session.hooks):
for file_name, config_data in self.session.hooks[state]:
file_data = FileData(
message_type=MessageFlags.ADD,
name=str(file_name),
type=f"hook:{state}",
type=f"hook:{state.value}",
data=str(config_data),
)
self.session.broadcast_file(file_data)

View file

@ -84,8 +84,8 @@ class LinkTlvs(Enum):
IFACE2_MAC = 0x39
IFACE2_IP6 = 0x40
IFACE2_IP6_MASK = 0x41
INTERFACE1_NAME = 0x42
INTERFACE2_NAME = 0x43
IFACE1_NAME = 0x42
IFACE2_NAME = 0x43
OPAQUE = 0x50

View file

@ -130,8 +130,8 @@ class EmaneModel(WirelessModel):
:return: nothing
"""
try:
wlan = self.session.get_node(self.id, EmaneNet)
wlan.setnempositions(moved_ifaces)
emane_net = self.session.get_node(self.id, EmaneNet)
emane_net.setnempositions(moved_ifaces)
except CoreError:
logging.exception("error during update")

View file

@ -8,6 +8,7 @@ import os
import pwd
import shutil
import subprocess
import sys
import tempfile
import threading
import time
@ -119,7 +120,7 @@ class Session:
# states and hooks handlers
self.state: EventTypes = EventTypes.DEFINITION_STATE
self.state_time: float = time.monotonic()
self.hooks: Dict[EventTypes, Tuple[str, str]] = {}
self.hooks: Dict[EventTypes, List[Tuple[str, str]]] = {}
self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {}
self.add_state_hook(
state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook
@ -621,7 +622,8 @@ class Session:
def is_active(self) -> bool:
"""
Determine if this session is considered to be active. (Runtime or Data collect states)
Determine if this session is considered to be active.
(Runtime or Data collect states)
:return: True if active, False otherwise
"""
@ -991,6 +993,7 @@ class Session:
:return: environment variables
"""
env = os.environ.copy()
env["CORE_PYTHON"] = sys.executable
env["SESSION"] = str(self.id)
env["SESSION_SHORT"] = self.short_session_id()
env["SESSION_DIR"] = self.session_dir
@ -1098,7 +1101,8 @@ class Session:
def delete_node(self, _id: int) -> bool:
"""
Delete a node from the session and check if session should shutdown, if no nodes are left.
Delete a node from the session and check if session should shutdown, if no nodes
are left.
:param _id: id of node to delete
:return: True if node deleted, False otherwise

View file

@ -111,13 +111,13 @@ class Application(ttk.Frame):
self.master.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.grid(sticky="nsew")
self.grid(sticky=tk.NSEW)
self.toolbar = Toolbar(self)
self.toolbar.grid(sticky="ns")
self.toolbar.grid(sticky=tk.NS)
self.right_frame = ttk.Frame(self)
self.right_frame.columnconfigure(0, weight=1)
self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew")
self.right_frame.grid(row=0, column=1, sticky=tk.NSEW)
self.draw_canvas()
self.draw_infobar()
self.draw_status()
@ -139,21 +139,21 @@ class Application(ttk.Frame):
canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
self.canvas = CanvasGraph(canvas_frame, self, self.core)
self.canvas.grid(sticky="nsew")
self.canvas.grid(sticky=tk.NSEW)
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
scroll_y.grid(row=0, column=1, sticky="ns")
scroll_y.grid(row=0, column=1, sticky=tk.NS)
scroll_x = ttk.Scrollbar(
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
)
scroll_x.grid(row=1, column=0, sticky="ew")
scroll_x.grid(row=1, column=0, sticky=tk.EW)
self.canvas.configure(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set)
def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew", columnspan=2)
self.statusbar.grid(sticky=tk.EW, columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get():
@ -161,7 +161,7 @@ class Application(ttk.Frame):
self.clear_info()
self.info_frame = frame_class(self.infobar, **kwargs)
self.info_frame.draw()
self.info_frame.grid(sticky="nsew")
self.info_frame.grid(sticky=tk.NSEW)
def clear_info(self) -> None:
if self.info_frame:
@ -174,7 +174,7 @@ class Application(ttk.Frame):
def show_info(self) -> None:
self.default_info()
self.infobar.grid(row=0, column=1, sticky="nsew")
self.infobar.grid(row=0, column=1, sticky=tk.NSEW)
def hide_info(self) -> None:
self.infobar.grid_forget()

File diff suppressed because it is too large Load diff

View file

@ -46,9 +46,9 @@ class AboutDialog(Dialog):
codetext = CodeText(self.top)
codetext.text.insert("1.0", LICENSE)
codetext.text.config(state=tk.DISABLED)
codetext.grid(sticky="nsew")
codetext.grid(sticky=tk.NSEW)
label = ttk.Label(
self.top, text="Icons from https://icons8.com", anchor=tk.CENTER
)
label.grid(sticky="ew")
label.grid(sticky=tk.EW)

View file

@ -5,10 +5,10 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional
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
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
if TYPE_CHECKING:
from core.gui.app import Application
@ -30,13 +30,13 @@ class AlertsDialog(Dialog):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
self.tree = ttk.Treeview(
frame,
columns=("time", "level", "session_id", "node", "source"),
show="headings",
)
self.tree.grid(row=0, column=0, sticky="nsew")
self.tree.grid(row=0, column=0, sticky=tk.NSEW)
self.tree.column("time", stretch=tk.YES)
self.tree.heading("time", text="Time")
self.tree.column("level", stretch=tk.YES, width=100)
@ -49,9 +49,8 @@ class AlertsDialog(Dialog):
self.tree.heading("source", text="Source")
self.tree.bind("<<TreeviewSelect>>", self.click_select)
for alarm in self.app.statusbar.core_alarms:
exception = alarm.exception_event
level_name = ExceptionLevel.Enum.Name(exception.level)
for exception in self.app.statusbar.core_alarms:
level_name = exception.level.name
node_id = exception.node_id if exception.node_id else ""
insert_id = self.tree.insert(
"",
@ -60,43 +59,43 @@ class AlertsDialog(Dialog):
values=(
exception.date,
level_name,
alarm.session_id,
exception.session_id,
node_id,
exception.source,
),
tags=(level_name,),
)
self.alarm_map[insert_id] = alarm
self.alarm_map[insert_id] = exception
error_name = ExceptionLevel.Enum.Name(ExceptionLevel.ERROR)
error_name = ExceptionLevel.ERROR.name
self.tree.tag_configure(error_name, background="#ff6666")
fatal_name = ExceptionLevel.Enum.Name(ExceptionLevel.FATAL)
fatal_name = ExceptionLevel.FATAL.name
self.tree.tag_configure(fatal_name, background="#d9d9d9")
warning_name = ExceptionLevel.Enum.Name(ExceptionLevel.WARNING)
warning_name = ExceptionLevel.WARNING.name
self.tree.tag_configure(warning_name, background="#ffff99")
notice_name = ExceptionLevel.Enum.Name(ExceptionLevel.NOTICE)
notice_name = ExceptionLevel.NOTICE.name
self.tree.tag_configure(notice_name, background="#85e085")
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
xscrollbar.grid(row=1, sticky=tk.EW)
self.tree.configure(xscrollcommand=xscrollbar.set)
self.codetext = CodeText(self.top)
self.codetext.text.config(state=tk.DISABLED, height=11)
self.codetext.grid(sticky="nsew", pady=PADY)
self.codetext.grid(sticky=tk.NSEW, pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def reset_alerts(self) -> None:
self.codetext.text.config(state=tk.NORMAL)
@ -108,8 +107,8 @@ class AlertsDialog(Dialog):
def click_select(self, event: tk.Event) -> None:
current = self.tree.selection()[0]
alarm = self.alarm_map[current]
exception = self.alarm_map[current]
self.codetext.text.config(state=tk.NORMAL)
self.codetext.text.delete(1.0, tk.END)
self.codetext.text.insert(1.0, alarm.exception_event.text)
self.codetext.text.insert(1.0, exception.text)
self.codetext.text.config(state=tk.DISABLED)

View file

@ -23,11 +23,11 @@ class SizeAndScaleDialog(Dialog):
"""
super().__init__(app, "Canvas Size and Scale")
self.canvas: CanvasGraph = self.app.canvas
self.section_font: font.Font = font.Font(weight="bold")
self.section_font: font.Font = font.Font(weight=font.BOLD)
width, height = self.canvas.current_dimensions
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
location = self.app.core.location
location = self.app.core.session.location
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)
@ -54,68 +54,68 @@ class SizeAndScaleDialog(Dialog):
def draw_size(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.grid(sticky=tk.EW)
label_frame.columnconfigure(0, weight=1)
# draw size row 1
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.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)
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Pixels")
label.grid(row=0, column=4, sticky="w")
label.grid(row=0, column=4, sticky=tk.W)
# draw size row 2
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_width, state=tk.DISABLED
)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
label = ttk.Label(frame, text="x Height")
label.grid(row=0, column=2, sticky="w", padx=PADX)
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
entry = validation.PositiveFloatEntry(
frame, textvariable=self.meters_height, state=tk.DISABLED
)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=4, sticky="w")
label.grid(row=0, column=4, sticky=tk.W)
def draw_scale(self) -> None:
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.grid(sticky=tk.EW)
label_frame.columnconfigure(0, weight=1)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
entry.bind("<KeyRelease>", self.size_scale_keyup)
label = ttk.Label(frame, text="Meters")
label.grid(row=0, column=2, sticky="w")
label.grid(row=0, column=2, sticky=tk.W)
def draw_reference_point(self) -> None:
label_frame = ttk.Labelframe(
self.top, text="Reference Point", padding=FRAME_PAD
)
label_frame.grid(sticky="ew")
label_frame.grid(sticky=tk.EW)
label_frame.columnconfigure(0, weight=1)
label = ttk.Label(
@ -124,61 +124,61 @@ class SizeAndScaleDialog(Dialog):
label.grid()
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="X")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.x)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
label = ttk.Label(frame, text="Y")
label.grid(row=0, column=2, sticky="w", padx=PADX)
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
entry = validation.PositiveFloatEntry(frame, textvariable=self.y)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
label = ttk.Label(label_frame, text="Translates To")
label.grid()
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
label = ttk.Label(frame, text="Lat")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = validation.FloatEntry(frame, textvariable=self.lat)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
label = ttk.Label(frame, text="Lon")
label.grid(row=0, column=2, sticky="w", padx=PADX)
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
entry = validation.FloatEntry(frame, textvariable=self.lon)
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
label = ttk.Label(frame, text="Alt")
label.grid(row=0, column=4, sticky="w", padx=PADX)
label.grid(row=0, column=4, sticky=tk.W, padx=PADX)
entry = validation.FloatEntry(frame, textvariable=self.alt)
entry.grid(row=0, column=5, sticky="ew")
entry.grid(row=0, column=5, sticky=tk.EW)
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)
button.grid(sticky=tk.W, pady=PADY)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def size_scale_keyup(self, _event: tk.Event) -> None:
scale = self.scale.get()
@ -192,7 +192,7 @@ class SizeAndScaleDialog(Dialog):
self.canvas.redraw_canvas((width, height))
if self.canvas.wallpaper:
self.canvas.redraw_wallpaper()
location = self.app.core.location
location = self.app.core.session.location
location.x = self.x.get()
location.y = self.y.get()
location.lat = self.lat.get()

View file

@ -51,7 +51,7 @@ class CanvasWallpaperDialog(Dialog):
def draw_image_label(self) -> None:
label = ttk.Label(self.top, text="Image filename: ")
label.grid(sticky="ew")
label.grid(sticky=tk.EW)
if self.filename.get():
self.draw_preview()
@ -60,17 +60,17 @@ class CanvasWallpaperDialog(Dialog):
frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.filename)
entry.focus()
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="...", command=self.click_open_image)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Clear", command=self.click_clear)
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
def draw_options(self) -> None:
frame = ttk.Frame(self.top)
@ -78,30 +78,30 @@ class CanvasWallpaperDialog(Dialog):
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.columnconfigure(3, weight=1)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW, pady=PADY)
button = ttk.Radiobutton(
frame, text="upper-left", value=1, variable=self.scale_option
)
button.grid(row=0, column=0, sticky="ew")
button.grid(row=0, column=0, sticky=tk.EW)
self.options.append(button)
button = ttk.Radiobutton(
frame, text="centered", value=2, variable=self.scale_option
)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
self.options.append(button)
button = ttk.Radiobutton(
frame, text="scaled", value=3, variable=self.scale_option
)
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
self.options.append(button)
button = ttk.Radiobutton(
frame, text="titled", value=4, variable=self.scale_option
)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
self.options.append(button)
def draw_additional_options(self) -> None:
@ -111,19 +111,19 @@ class CanvasWallpaperDialog(Dialog):
variable=self.adjust_to_dim,
command=self.click_adjust_canvas,
)
checkbutton.grid(sticky="ew", padx=PADX)
checkbutton.grid(sticky=tk.EW, padx=PADX, pady=PADY)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_open_image(self) -> None:
filename = image_chooser(self, BACKGROUNDS_PATH)

View file

@ -48,13 +48,13 @@ class ColorPickerDialog(Dialog):
# rgb frames
frame = ttk.Frame(self.top)
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
frame.grid(row=0, column=0, sticky=tk.EW, pady=PADY)
frame.columnconfigure(2, weight=3)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="R")
label.grid(row=0, column=0, padx=PADX)
self.red_entry = validation.RgbEntry(frame, width=3, textvariable=self.red)
self.red_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
self.red_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
scale = ttk.Scale(
frame,
from_=0,
@ -64,20 +64,20 @@ class ColorPickerDialog(Dialog):
variable=self.red_scale,
command=lambda x: self.scale_callback(self.red_scale, self.red),
)
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.red_label = ttk.Label(
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
)
self.red_label.grid(row=0, column=3, sticky="ew")
self.red_label.grid(row=0, column=3, sticky=tk.EW)
frame = ttk.Frame(self.top)
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY)
frame.columnconfigure(2, weight=3)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="G")
label.grid(row=0, column=0, padx=PADX)
self.green_entry = validation.RgbEntry(frame, width=3, textvariable=self.green)
self.green_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
self.green_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
scale = ttk.Scale(
frame,
from_=0,
@ -87,20 +87,20 @@ class ColorPickerDialog(Dialog):
variable=self.green_scale,
command=lambda x: self.scale_callback(self.green_scale, self.green),
)
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.green_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
)
self.green_label.grid(row=0, column=3, sticky="ew")
self.green_label.grid(row=0, column=3, sticky=tk.EW)
frame = ttk.Frame(self.top)
frame.grid(row=2, column=0, sticky="ew", pady=PADY)
frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
frame.columnconfigure(2, weight=3)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="B")
label.grid(row=0, column=0, padx=PADX)
self.blue_entry = validation.RgbEntry(frame, width=3, textvariable=self.blue)
self.blue_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
self.blue_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
scale = ttk.Scale(
frame,
from_=0,
@ -110,31 +110,31 @@ class ColorPickerDialog(Dialog):
variable=self.blue_scale,
command=lambda x: self.scale_callback(self.blue_scale, self.blue),
)
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
self.blue_label = ttk.Label(
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
)
self.blue_label.grid(row=0, column=3, sticky="ew")
self.blue_label.grid(row=0, column=3, sticky=tk.EW)
# hex code and color display
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
self.hex_entry = validation.HexEntry(frame, textvariable=self.hex)
self.hex_entry.grid(sticky="ew", pady=PADY)
self.hex_entry.grid(sticky=tk.EW, pady=PADY)
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
self.display.grid(sticky="nsew")
frame.grid(row=3, column=0, sticky="nsew", pady=PADY)
self.display.grid(sticky=tk.NSEW)
frame.grid(row=3, column=0, sticky=tk.NSEW, pady=PADY)
# button frame
frame = ttk.Frame(self.top)
frame.grid(row=4, column=0, sticky="ew")
frame.grid(row=4, column=0, sticky=tk.EW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="OK", command=self.button_ok)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def set_bindings(self) -> None:
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))

View file

@ -8,32 +8,29 @@ 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
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
from core.gui.wrappers import (
ConfigOption,
ConfigServiceData,
Node,
ServiceValidationMode,
)
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):
def __init__(
self,
master: tk.BaseWidget,
app: "Application",
service_name: str,
canvas_node: "CanvasNode",
node_id: int,
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
) -> None:
title = f"{service_name} Config Service"
super().__init__(app, title, master=master)
self.core: "CoreClient" = app.core
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = node_id
self.node: Node = node
self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(2)
@ -51,7 +48,7 @@ class ConfigServiceConfigDialog(Dialog):
self.validation_time: Optional[int] = None
self.validation_period: tk.StringVar = tk.StringVar()
self.modes: List[str] = []
self.mode_configs: Dict[str, str] = {}
self.mode_configs: Dict[str, Dict[str, str]] = {}
self.notebook: Optional[ttk.Notebook] = None
self.templates_combobox: Optional[ttk.Combobox] = None
@ -92,25 +89,18 @@ class ConfigServiceConfigDialog(Dialog):
response = self.core.client.get_config_service_defaults(self.service_name)
self.original_service_files = response.templates
self.temp_service_files = dict(self.original_service_files)
self.modes = sorted(x.name for x in response.modes)
self.mode_configs = {x.name: x.config for x in response.modes}
service_config = self.canvas_node.config_service_configs.get(
self.service_name, {}
)
self.config = response.config
self.config = ConfigOption.from_dict(response.config)
self.default_config = {x.name: x.value for x in self.config.values()}
custom_config = service_config.get("config")
if custom_config:
for key, value in custom_config.items():
service_config = self.node.config_service_configs.get(self.service_name)
if service_config:
for key, value in service_config.config.items():
self.config[key].value = value
logging.info("default config: %s", self.default_config)
custom_templates = service_config.get("templates", {})
for file, data in custom_templates.items():
self.modified_files.add(file)
self.temp_service_files[file] = data
logging.info("default config: %s", self.default_config)
for file, data in service_config.templates.items():
self.modified_files.add(file)
self.temp_service_files[file] = data
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Config Service Error", e)
self.has_error = True
@ -121,7 +111,7 @@ class ConfigServiceConfigDialog(Dialog):
# draw notebook
self.notebook = ttk.Notebook(self.top)
self.notebook.grid(sticky="nsew", pady=PADY)
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
self.draw_tab_files()
if self.config:
self.draw_tab_config()
@ -131,7 +121,7 @@ class ConfigServiceConfigDialog(Dialog):
def draw_tab_files(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Directories/Files")
@ -141,29 +131,29 @@ class ConfigServiceConfigDialog(Dialog):
label.grid(pady=PADY)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Directories")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
directories_combobox = ttk.Combobox(
frame, values=self.directories, state="readonly"
)
directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
if self.directories:
directories_combobox.current(0)
label = ttk.Label(frame, text="Templates")
label.grid(row=1, column=0, sticky="w", padx=PADX)
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
self.templates_combobox = ttk.Combobox(
frame, values=self.templates, state="readonly"
)
self.templates_combobox.bind(
"<<ComboboxSelected>>", self.handle_template_changed
)
self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY)
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
self.template_text = CodeText(tab)
self.template_text.grid(sticky="nsew")
self.template_text.grid(sticky=tk.NSEW)
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
if self.templates:
self.templates_combobox.current(0)
@ -175,13 +165,13 @@ class ConfigServiceConfigDialog(Dialog):
def draw_tab_config(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Configuration")
if self.modes:
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Modes")
label.grid(row=0, column=0, padx=PADX)
@ -189,17 +179,17 @@ class ConfigServiceConfigDialog(Dialog):
frame, values=self.modes, state="readonly"
)
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
logging.info("config service config: %s", self.config)
self.config_frame = ConfigFrame(tab, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
def draw_tab_startstop(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
for i in range(3):
tab.rowconfigure(i, weight=1)
@ -225,12 +215,12 @@ class ConfigServiceConfigDialog(Dialog):
commands = self.validation_commands
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
listbox_scroll = ListboxScroll(label_frame)
for command in commands:
listbox_scroll.listbox.insert("end", command)
listbox_scroll.listbox.config(height=4)
listbox_scroll.grid(sticky="nsew")
listbox_scroll.grid(sticky=tk.NSEW)
if i == 0:
self.startup_commands_listbox = listbox_scroll.listbox
elif i == 1:
@ -240,23 +230,23 @@ class ConfigServiceConfigDialog(Dialog):
def draw_tab_validation(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="ew")
tab.grid(sticky=tk.EW)
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Validation", sticky="nsew")
self.notebook.add(tab, text="Validation", sticky=tk.NSEW)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Validation Time")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
self.validation_time_entry = ttk.Entry(frame)
self.validation_time_entry.insert("end", self.validation_time)
self.validation_time_entry.config(state=tk.DISABLED)
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Validation Mode")
label.grid(row=1, column=0, sticky="w", padx=PADX)
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
if self.validation_mode == ServiceValidationMode.BLOCKING:
mode = "BLOCKING"
elif self.validation_mode == ServiceValidationMode.NON_BLOCKING:
@ -268,66 +258,64 @@ class ConfigServiceConfigDialog(Dialog):
)
self.validation_mode_entry.insert("end", mode)
self.validation_mode_entry.config(state=tk.DISABLED)
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Validation Period")
label.grid(row=2, column=0, sticky="w", padx=PADX)
label.grid(row=2, column=0, sticky=tk.W, padx=PADX)
self.validation_period_entry = ttk.Entry(
frame, state=tk.DISABLED, textvariable=self.validation_period
)
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY)
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.grid(sticky=tk.NSEW, pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
listbox_scroll.grid(sticky=tk.NSEW)
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for executable in self.executables:
listbox_scroll.listbox.insert("end", executable)
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.grid(sticky=tk.NSEW, pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
listbox_scroll.grid(sticky=tk.NSEW)
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
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)
self.node.config_service_configs.pop(self.service_name, None)
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
self.destroy()
return
service_config = self.canvas_node.config_service_configs.setdefault(
self.service_name, {}
)
service_config = self.node.config_service_configs.get(self.service_name)
if not service_config:
service_config = ConfigServiceData()
if self.config_frame:
self.config_frame.parse_config()
service_config["config"] = {x.name: x.value for x in self.config.values()}
templates_config = service_config.setdefault("templates", {})
service_config.config = {x.name: x.value for x in self.config.values()}
for file in self.modified_files:
templates_config[file] = self.temp_service_files[file]
service_config.templates[file] = self.temp_service_files[file]
all_current = current_listbox.get(0, tk.END)
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
self.destroy()
@ -361,9 +349,9 @@ class ConfigServiceConfigDialog(Dialog):
return has_custom_templates or has_custom_config
def click_defaults(self) -> None:
self.canvas_node.config_service_configs.pop(self.service_name, None)
self.node.config_service_configs.pop(self.service_name, None)
logging.info(
"cleared config service config: %s", self.canvas_node.config_service_configs
"cleared config service config: %s", self.node.config_service_configs
)
self.temp_service_files = dict(self.original_service_files)
filename = self.templates_combobox.get()

View file

@ -38,41 +38,40 @@ class CopyServiceConfigDialog(Dialog):
label = ttk.Label(
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
)
label.grid(sticky="ew", pady=PADY)
label.grid(sticky=tk.EW, pady=PADY)
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.grid(sticky=tk.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)
for node in self.app.core.session.nodes.values():
file_configs = 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)
self.nodes[node.name] = node.id
self.listbox.insert(tk.END, node.name)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Copy", command=self.click_copy)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="View", command=self.click_view)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
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]
node_id = self.nodes[name]
node = self.app.core.session.nodes[node_id]
data = 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)
@ -84,9 +83,9 @@ class CopyServiceConfigDialog(Dialog):
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]
node_id = self.nodes[name]
node = self.app.core.session.nodes[node_id]
data = node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog(
self.app, self, name, self.service, self.file_name, data
)
@ -113,8 +112,8 @@ class ViewConfigDialog(Dialog):
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
self.service_data = CodeText(self.top)
self.service_data.grid(sticky="nsew", pady=PADY)
self.service_data.grid(sticky=tk.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(sticky="ew")
button.grid(sticky=tk.EW)

View file

@ -34,47 +34,47 @@ class ServicesSelectDialog(Dialog):
self.top.rowconfigure(0, weight=1)
frame = ttk.LabelFrame(self.top)
frame.grid(stick="nsew", pady=PADY)
frame.grid(stick=tk.NSEW, pady=PADY)
frame.rowconfigure(0, weight=1)
for i in range(3):
frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
label_frame.grid(row=0, column=0, sticky="nsew")
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew")
self.groups.grid(sticky=tk.NSEW)
for group in sorted(self.app.core.services):
self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services")
label_frame.grid(row=0, column=1, sticky="nsew")
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
)
self.services.grid(sticky="nsew")
self.services.grid(sticky=tk.NSEW)
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
self.current.grid(sticky=tk.NSEW)
for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service)
frame = ttk.Frame(self.top)
frame.grid(stick="ew")
frame.grid(stick=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.destroy)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
# trigger group change
self.groups.listbox.event_generate("<<ListboxSelect>>")
@ -127,58 +127,58 @@ class CustomNodesDialog(Dialog):
def draw_node_config(self) -> None:
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
self.nodes_list = ListboxScroll(frame)
self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX)
self.nodes_list.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX)
self.nodes_list.listbox.bind("<<ListboxSelect>>", self.handle_node_select)
for name in sorted(self.app.core.custom_nodes):
self.nodes_list.listbox.insert(tk.END, name)
frame = ttk.Frame(frame)
frame.grid(row=0, column=2, sticky="nsew")
frame.grid(row=0, column=2, sticky=tk.NSEW)
frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(sticky="ew", pady=PADY)
entry.grid(sticky=tk.EW, pady=PADY)
self.image_button = ttk.Button(
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
)
self.image_button.grid(sticky="ew", pady=PADY)
self.image_button.grid(sticky=tk.EW, pady=PADY)
button = ttk.Button(frame, text="Services", command=self.click_services)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
def draw_node_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
self.edit_button = ttk.Button(
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
)
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def reset_values(self) -> None:
self.name.set("")

View file

@ -30,7 +30,7 @@ class Dialog(tk.Toplevel):
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
self.top.grid(sticky="nsew")
self.top.grid(sticky=tk.NSEW)
def show(self) -> None:
self.transient(self.master)
@ -44,6 +44,6 @@ class Dialog(tk.Toplevel):
def draw_spacer(self, row: int = None) -> None:
frame = ttk.Frame(self.top)
frame.grid(row=row, sticky="nsew")
frame.grid(row=row, sticky=tk.NSEW)
frame.rowconfigure(0, weight=1)
self.top.rowconfigure(frame.grid_info()["row"], weight=1)

View file

@ -8,16 +8,14 @@ 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
from core.gui.widgets import ConfigFrame
from core.gui.wrappers import ConfigOption, Node
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class GlobalEmaneDialog(Dialog):
@ -30,24 +28,24 @@ class GlobalEmaneDialog(Dialog):
def draw(self) -> None:
self.top.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1)
session = self.app.core.session
self.config_frame = ConfigFrame(
self.top, self.app, self.app.core.emane_config, self.enabled
self.top, self.app, session.emane_config, self.enabled
)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.draw_spacer()
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
self.draw_buttons()
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_apply(self) -> None:
self.config_frame.parse_config()
@ -59,24 +57,19 @@ class EmaneModelDialog(Dialog):
self,
master: tk.BaseWidget,
app: "Application",
canvas_node: "CanvasNode",
node: Node,
model: str,
iface_id: int = None,
) -> None:
super().__init__(
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
)
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
super().__init__(app, f"{node.name} {model} Configuration", master=master)
self.node: Node = 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:
config = self.canvas_node.emane_model_configs.get(
(self.model, self.iface_id)
)
config = self.node.emane_model_configs.get((self.model, self.iface_id))
if not config:
config = self.app.core.get_emane_model_config(
self.node.id, self.model, self.iface_id
@ -93,37 +86,35 @@ class EmaneModelDialog(Dialog):
self.top.rowconfigure(0, weight=1)
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.config_frame.grid(sticky=tk.NSEW, pady=PADY)
self.draw_buttons()
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_apply(self) -> None:
self.config_frame.parse_config()
key = (self.model, self.iface_id)
self.canvas_node.emane_model_configs[key] = self.config
self.node.emane_model_configs[key] = self.config
self.destroy()
class EmaneConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
def __init__(self, app: "Application", node: Node) -> None:
super().__init__(app, f"{node.name} EMANE Configuration")
self.node: Node = node
self.radiovar: tk.IntVar = tk.IntVar()
self.radiovar.set(1)
self.emane_models: List[str] = [
x.split("_")[1] for x in self.app.core.emane_models
x.split("_")[1] for x in self.app.core.session.emane_models
]
model = self.node.emane.split("_")[1]
self.emane_model: tk.StringVar = tk.StringVar(value=model)
@ -163,30 +154,30 @@ class EmaneConfigDialog(Dialog):
),
)
button.image = image
button.grid(sticky="ew", pady=PADY)
button.grid(sticky=tk.EW, pady=PADY)
def draw_emane_models(self) -> None:
"""
create a combobox that has all the known emane models
"""
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Model")
label.grid(row=0, column=0, sticky="w")
label.grid(row=0, column=0, sticky=tk.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=state
)
combobox.grid(row=0, column=1, sticky="ew")
combobox.grid(row=0, column=1, sticky=tk.EW)
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
def draw_emane_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
for i in range(2):
frame.columnconfigure(i, weight=1)
@ -199,7 +190,7 @@ class EmaneConfigDialog(Dialog):
command=self.click_model_config,
)
self.emane_model_button.image = image
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew")
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.EDITNODE, 16)
button = ttk.Button(
@ -210,18 +201,18 @@ class EmaneConfigDialog(Dialog):
command=self.click_emane_config,
)
button.image = image
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def draw_apply_and_cancel(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
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.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_emane_config(self) -> None:
dialog = GlobalEmaneDialog(self, self.app)
@ -232,7 +223,7 @@ class EmaneConfigDialog(Dialog):
draw emane model configuration
"""
model_name = self.emane_model.get()
dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name)
dialog = EmaneModelDialog(self, self.app, self.node, model_name)
if not dialog.has_error:
dialog.show()

View file

@ -1,3 +1,4 @@
import tkinter as tk
import webbrowser
from tkinter import ttk
@ -13,13 +14,13 @@ class EmaneInstallDialog(Dialog):
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)
label.grid(sticky=tk.EW, pady=PADY)
button = ttk.Button(
self.top, text="EMANE Documentation", command=self.click_doc
)
button.grid(sticky="ew", pady=PADY)
button.grid(sticky=tk.EW, pady=PADY)
button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
def click_doc(self) -> None:
webbrowser.open_new("https://coreemu.github.io/core/emane.html")

View file

@ -25,13 +25,13 @@ class ExecutePythonDialog(Dialog):
frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=i, column=0, sticky="nsew")
frame.grid(row=i, column=0, sticky=tk.NSEW)
i = i + 1
var = tk.StringVar(value="")
self.file_entry = ttk.Entry(frame, textvariable=var)
self.file_entry.grid(row=0, column=0, sticky="ew")
self.file_entry.grid(row=0, column=0, sticky=tk.EW)
button = ttk.Button(frame, text="...", command=self.select_file)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
self.top.columnconfigure(0, weight=1)
button = ttk.Checkbutton(
@ -40,18 +40,18 @@ class ExecutePythonDialog(Dialog):
variable=self.with_options,
command=self.add_options,
)
button.grid(row=i, column=0, sticky="ew")
button.grid(row=i, column=0, sticky=tk.EW)
i = i + 1
label = ttk.Label(
self.top, text="Any command-line options for running the Python script"
)
label.grid(row=i, column=0, sticky="ew")
label.grid(row=i, column=0, sticky=tk.EW)
i = i + 1
self.option_entry = ttk.Entry(
self.top, textvariable=self.options, state="disabled"
)
self.option_entry.grid(row=i, column=0, sticky="ew")
self.option_entry.grid(row=i, column=0, sticky=tk.EW)
i = i + 1
frame = ttk.Frame(self.top, padding=FRAME_PAD)
@ -59,9 +59,9 @@ class ExecutePythonDialog(Dialog):
frame.columnconfigure(1, weight=1)
frame.grid(row=i, column=0)
button = ttk.Button(frame, text="Execute", command=self.script_execute)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
def add_options(self) -> None:
if self.with_options.get():

View file

@ -25,25 +25,25 @@ class FindDialog(Dialog):
# Find node frame
frame = ttk.Frame(self.top, padding=FRAME_PAD)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Find:")
label.grid()
entry = ttk.Entry(frame, textvariable=self.find_text)
entry.grid(row=0, column=1, sticky="nsew")
entry.grid(row=0, column=1, sticky=tk.NSEW)
# node list frame
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
self.tree = ttk.Treeview(
frame,
columns=("nodeid", "name", "location", "detail"),
show="headings",
selectmode=tk.BROWSE,
)
self.tree.grid(sticky="nsew", pady=PADY)
self.tree.grid(sticky=tk.NSEW, pady=PADY)
style = ttk.Style()
heading_size = int(self.app.guiconfig.scale * 10)
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
@ -57,21 +57,21 @@ class FindDialog(Dialog):
self.tree.heading("detail", text="Detail")
self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
xscrollbar.grid(row=1, sticky=tk.EW)
self.tree.configure(xscrollcommand=xscrollbar.set)
# button frame
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Find", command=self.find_node)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.close_dialog)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def clear_treeview_items(self) -> None:
"""
@ -87,22 +87,19 @@ class FindDialog(Dialog):
"""
node_name = self.find_text.get().strip()
self.clear_treeview_items()
for node_id, node in sorted(
self.app.core.canvas_nodes.items(), key=lambda x: x[0]
):
name = node.core_node.name
for node in self.app.core.session.nodes.values():
name = node.name
if not node_name or node_name == name:
pos_x = round(node.core_node.position.x, 1)
pos_y = round(node.core_node.position.y, 1)
pos_x = round(node.position.x, 1)
pos_y = round(node.position.y, 1)
# TODO: I am not sure what to insert for Detail column
# leaving it blank for now
self.tree.insert(
"",
tk.END,
text=str(node_id),
values=(node_id, name, f"<{pos_x}, {pos_y}>", ""),
text=str(node.id),
values=(node.id, name, f"<{pos_x}, {pos_y}>", ""),
)
results = self.tree.get_children("")
if results:
self.tree.selection_set(results[0])
@ -121,7 +118,7 @@ class FindDialog(Dialog):
if item:
self.app.canvas.delete("find")
node_id = int(self.tree.item(item, "text"))
canvas_node = self.app.core.canvas_nodes[node_id]
canvas_node = self.app.core.get_canvas_node(node_id)
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
dist = 5 * self.app.guiconfig.scale

View file

@ -2,10 +2,10 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
from core.gui.wrappers import Hook, SessionState
if TYPE_CHECKING:
from core.gui.app import Application
@ -16,8 +16,9 @@ class HookDialog(Dialog):
super().__init__(app, "Hook", master=master)
self.name: tk.StringVar = tk.StringVar()
self.codetext: Optional[CodeText] = None
self.hook: core_pb2.Hook = core_pb2.Hook()
self.hook: Optional[Hook] = None
self.state: tk.StringVar = tk.StringVar()
self.editing: bool = False
self.draw()
def draw(self) -> None:
@ -26,22 +27,22 @@ class HookDialog(Dialog):
# name and states
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(0, weight=2)
frame.columnconfigure(1, weight=7)
frame.columnconfigure(2, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE")
initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
values = tuple(x.name for x in SessionState)
initial_state = SessionState.RUNTIME.name
self.state.set(initial_state)
self.name.set(f"{initial_state.lower()}_hook.sh")
combobox = ttk.Combobox(
frame, textvariable=self.state, values=values, state="readonly"
)
combobox.grid(row=0, column=2, sticky="ew")
combobox.grid(row=0, column=2, sticky=tk.EW)
combobox.bind("<<ComboboxSelected>>", self.state_change)
# data
@ -54,36 +55,43 @@ class HookDialog(Dialog):
"# specified state\n"
),
)
self.codetext.grid(sticky="nsew", pady=PADY)
self.codetext.grid(sticky=tk.NSEW, pady=PADY)
# button row
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=lambda: self.save())
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def state_change(self, event: tk.Event) -> None:
if self.editing:
return
state_name = self.state.get()
self.name.set(f"{state_name.lower()}_hook.sh")
def set(self, hook: core_pb2.Hook) -> None:
def set(self, hook: Hook) -> None:
self.editing = True
self.hook = hook
self.name.set(hook.file)
self.codetext.text.delete(1.0, tk.END)
self.codetext.text.insert(tk.END, hook.data)
state_name = core_pb2.SessionState.Enum.Name(hook.state)
state_name = hook.state.name
self.state.set(state_name)
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()
self.hook.data = data
self.hook.state = state_value
state = SessionState[self.state.get()]
file_name = self.name.get()
if self.editing:
self.hook.state = state
self.hook.file = file_name
self.hook.data = data
else:
self.hook = Hook(state=state, file=file_name, data=data)
self.destroy()
@ -94,6 +102,7 @@ class HooksDialog(Dialog):
self.edit_button: Optional[ttk.Button] = None
self.delete_button: Optional[ttk.Button] = None
self.selected: Optional[str] = None
self.selected_index: Optional[int] = None
self.draw()
def draw(self) -> None:
@ -101,56 +110,65 @@ class HooksDialog(Dialog):
self.top.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
self.listbox = listbox_scroll.listbox
self.listbox.bind("<<ListboxSelect>>", self.select)
for hook_file in self.app.core.hooks:
self.listbox.insert(tk.END, hook_file)
session = self.app.core.session
for file in session.hooks:
self.listbox.insert(tk.END, file)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
self.edit_button = ttk.Button(
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
)
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX)
self.delete_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
def click_create(self) -> None:
dialog = HookDialog(self, self.app)
dialog.show()
hook = dialog.hook
if hook:
self.app.core.hooks[hook.file] = hook
self.app.core.session.hooks[hook.file] = hook
self.listbox.insert(tk.END, hook.file)
def click_edit(self) -> None:
hook = self.app.core.hooks[self.selected]
session = self.app.core.session
hook = session.hooks.pop(self.selected)
dialog = HookDialog(self, self.app)
dialog.set(hook)
dialog.show()
session.hooks[hook.file] = hook
self.selected = hook.file
self.listbox.delete(self.selected_index)
self.listbox.insert(self.selected_index, hook.file)
self.listbox.select_set(self.selected_index)
def click_delete(self) -> None:
del self.app.core.hooks[self.selected]
self.listbox.delete(tk.ANCHOR)
session = self.app.core.session
del session.hooks[self.selected]
self.listbox.delete(self.selected_index)
self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)
def select(self, event: tk.Event) -> None:
if self.listbox.curselection():
index = self.listbox.curselection()[0]
self.selected = self.listbox.get(index)
self.selected_index = self.listbox.curselection()[0]
self.selected = self.listbox.get(self.selected_index)
self.edit_button.config(state=tk.NORMAL)
self.delete_button.config(state=tk.NORMAL)
else:
self.selected = None
self.selected_index = None
self.edit_button.config(state=tk.DISABLED)
self.delete_button.config(state=tk.DISABLED)

View file

@ -34,7 +34,7 @@ class IpConfigDialog(Dialog):
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD)
ip4_frame.columnconfigure(0, weight=1)
@ -42,23 +42,23 @@ class IpConfigDialog(Dialog):
ip4_frame.grid(row=0, column=0, stick="nsew")
self.ip4_listbox = ListboxScroll(ip4_frame)
self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4)
self.ip4_listbox.grid(sticky="nsew", pady=PADY)
self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY)
for index, ip4 in enumerate(self.ip4s):
self.ip4_listbox.listbox.insert(tk.END, ip4)
if self.ip4 == ip4:
self.ip4_listbox.listbox.select_set(index)
self.ip4_entry = ttk.Entry(ip4_frame)
self.ip4_entry.grid(sticky="ew", pady=PADY)
self.ip4_entry.grid(sticky=tk.EW, pady=PADY)
ip4_button_frame = ttk.Frame(ip4_frame)
ip4_button_frame.columnconfigure(0, weight=1)
ip4_button_frame.columnconfigure(1, weight=1)
ip4_button_frame.grid(sticky="ew")
ip4_button_frame.grid(sticky=tk.EW)
ip4_add = ttk.Button(ip4_button_frame, text="Add", command=self.click_add_ip4)
ip4_add.grid(row=0, column=0, sticky="ew")
ip4_add.grid(row=0, column=0, sticky=tk.EW)
ip4_del = ttk.Button(
ip4_button_frame, text="Delete", command=self.click_del_ip4
)
ip4_del.grid(row=0, column=1, sticky="ew")
ip4_del.grid(row=0, column=1, sticky=tk.EW)
ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD)
ip6_frame.columnconfigure(0, weight=1)
@ -66,23 +66,23 @@ class IpConfigDialog(Dialog):
ip6_frame.grid(row=0, column=1, stick="nsew")
self.ip6_listbox = ListboxScroll(ip6_frame)
self.ip6_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip6)
self.ip6_listbox.grid(sticky="nsew", pady=PADY)
self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY)
for index, ip6 in enumerate(self.ip6s):
self.ip6_listbox.listbox.insert(tk.END, ip6)
if self.ip6 == ip6:
self.ip6_listbox.listbox.select_set(index)
self.ip6_entry = ttk.Entry(ip6_frame)
self.ip6_entry.grid(sticky="ew", pady=PADY)
self.ip6_entry.grid(sticky=tk.EW, pady=PADY)
ip6_button_frame = ttk.Frame(ip6_frame)
ip6_button_frame.columnconfigure(0, weight=1)
ip6_button_frame.columnconfigure(1, weight=1)
ip6_button_frame.grid(sticky="ew")
ip6_button_frame.grid(sticky=tk.EW)
ip6_add = ttk.Button(ip6_button_frame, text="Add", command=self.click_add_ip6)
ip6_add.grid(row=0, column=0, sticky="ew")
ip6_add.grid(row=0, column=0, sticky=tk.EW)
ip6_del = ttk.Button(
ip6_button_frame, text="Delete", command=self.click_del_ip6
)
ip6_del.grid(row=0, column=1, sticky="ew")
ip6_del.grid(row=0, column=1, sticky=tk.EW)
# draw buttons
frame = ttk.Frame(self.top)
@ -90,9 +90,9 @@ class IpConfigDialog(Dialog):
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_add_ip4(self) -> None:
ip4 = self.ip4_entry.get()

View file

@ -5,11 +5,11 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Optional
from core.api.grpc import core_pb2
from core.gui import validation
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY
from core.gui.wrappers import Interface, Link, LinkOptions
if TYPE_CHECKING:
from core.gui.app import Application
@ -21,7 +21,7 @@ def get_int(var: tk.StringVar) -> Optional[int]:
if value != "":
return int(value)
else:
return None
return 0
def get_float(var: tk.StringVar) -> Optional[float]:
@ -29,14 +29,15 @@ def get_float(var: tk.StringVar) -> Optional[float]:
if value != "":
return float(value)
else:
return None
return 0.0
class LinkConfigurationDialog(Dialog):
def __init__(self, app: "Application", edge: "CanvasEdge") -> None:
super().__init__(app, "Link Configuration")
self.edge: "CanvasEdge" = edge
self.is_symmetric: bool = edge.link.options.unidirectional is False
self.is_symmetric: bool = edge.link.is_symmetric()
if self.is_symmetric:
symmetry_var = tk.StringVar(value=">>")
else:
@ -72,14 +73,11 @@ class LinkConfigurationDialog(Dialog):
label = ttk.Label(
self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER
)
label.grid(row=0, column=0, sticky="ew", pady=PADY)
label.grid(row=0, column=0, sticky=tk.EW, pady=PADY)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
button = ttk.Button(frame, text="Unlimited")
button.grid(row=0, column=0, sticky="ew", padx=PADX)
frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY)
if self.is_symmetric:
button = ttk.Button(
frame, textvariable=self.symmetry_var, command=self.change_symmetry
@ -88,25 +86,25 @@ class LinkConfigurationDialog(Dialog):
button = ttk.Button(
frame, textvariable=self.symmetry_var, command=self.change_symmetry
)
button.grid(row=0, column=1, sticky="ew")
button.grid(sticky=tk.EW)
if self.is_symmetric:
self.symmetric_frame = self.get_frame()
self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
self.symmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
else:
self.asymmetric_frame = self.get_frame()
self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
self.asymmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
self.draw_spacer(row=3)
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.grid(row=4, column=0, sticky="ew")
frame.grid(row=4, column=0, sticky=tk.EW)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def get_frame(self) -> ttk.Frame:
frame = ttk.Frame(self.top)
@ -117,76 +115,76 @@ class LinkConfigurationDialog(Dialog):
label_name = "Asymmetric Effects: Downstream / Upstream "
row = 0
label = ttk.Label(frame, text=label_name, anchor=tk.CENTER)
label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY)
label.grid(row=row, column=0, columnspan=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Bandwidth (bps)")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.bandwidth
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
if not self.is_symmetric:
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_bandwidth
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Delay (us)")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.delay
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
if not self.is_symmetric:
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_delay
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Jitter (us)")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.jitter
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
if not self.is_symmetric:
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_jitter
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Loss (%)")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.loss
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
if not self.is_symmetric:
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.down_loss
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Duplicate (%)")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.duplicate
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
if not self.is_symmetric:
entry = validation.PositiveIntEntry(
frame, empty_enabled=False, textvariable=self.down_duplicate
)
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Color")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
self.color_button = tk.Button(
frame,
textvariable=self.color,
@ -196,15 +194,15 @@ class LinkConfigurationDialog(Dialog):
highlightthickness=0,
command=self.click_color,
)
self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY)
self.color_button.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
row = row + 1
label = ttk.Label(frame, text="Width")
label.grid(row=row, column=0, sticky="ew")
label.grid(row=row, column=0, sticky=tk.EW)
entry = validation.PositiveFloatEntry(
frame, empty_enabled=False, textvariable=self.width
)
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
return frame
@ -223,32 +221,26 @@ class LinkConfigurationDialog(Dialog):
delay = get_int(self.delay)
duplicate = get_int(self.duplicate)
loss = get_float(self.loss)
options = core_pb2.LinkOptions(
options = LinkOptions(
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss
)
link.options.CopyFrom(options)
iface1_id = None
if link.HasField("iface1"):
iface1_id = link.iface1.id
iface2_id = None
if link.HasField("iface2"):
iface2_id = link.iface2.id
link.options = options
iface1_id = link.iface1.id if link.iface1 else None
iface2_id = link.iface2.id if link.iface2 else None
if not self.is_symmetric:
link.options.unidirectional = True
asym_iface1 = None
if iface1_id:
asym_iface1 = core_pb2.Interface(id=iface1_id)
if iface1_id is not None:
asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None
if iface2_id:
asym_iface2 = core_pb2.Interface(id=iface2_id)
if iface2_id is not None:
asym_iface2 = 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)
down_duplicate = get_int(self.down_duplicate)
down_loss = get_float(self.down_loss)
options = core_pb2.LinkOptions(
options = LinkOptions(
bandwidth=down_bandwidth,
jitter=down_jitter,
delay=down_delay,
@ -256,36 +248,21 @@ class LinkConfigurationDialog(Dialog):
loss=down_loss,
unidirectional=True,
)
self.edge.asymmetric_link = core_pb2.Link(
self.edge.asymmetric_link = Link(
node1_id=link.node2_id,
node2_id=link.node1_id,
iface1=asym_iface1,
iface2=asym_iface2,
iface1=asym_iface2,
iface2=asym_iface1,
options=options,
)
else:
link.options.unidirectional = False
self.edge.asymmetric_link = None
if self.app.core.is_runtime() and link.HasField("options"):
session_id = self.app.core.session_id
self.app.core.client.edit_link(
session_id,
link.node1_id,
link.node2_id,
link.options,
iface1_id,
iface2_id,
)
if self.app.core.is_runtime() and link.options:
self.app.core.edit_link(link)
if self.edge.asymmetric_link:
self.app.core.client.edit_link(
session_id,
link.node2_id,
link.node1_id,
self.edge.asymmetric_link.options,
iface1_id,
iface2_id,
)
self.app.core.edit_link(self.edge.asymmetric_link)
# update edge label
self.edge.draw_link_options()
@ -316,7 +293,7 @@ class LinkConfigurationDialog(Dialog):
color = self.app.canvas.itemcget(self.edge.id, "fill")
self.color.set(color)
link = self.edge.link
if link.HasField("options"):
if link.options:
self.bandwidth.set(str(link.options.bandwidth))
self.jitter.set(str(link.options.jitter))
self.duplicate.set(str(link.options.dup))

View file

@ -28,7 +28,7 @@ class MacConfigDialog(Dialog):
"provided value below and increment by value in order."
)
label = ttk.Label(self.top, text=text)
label.grid(sticky="ew", pady=PADY)
label.grid(sticky=tk.EW, pady=PADY)
# draw input
frame = ttk.Frame(self.top)
@ -36,9 +36,9 @@ class MacConfigDialog(Dialog):
frame.columnconfigure(1, weight=3)
frame.grid(stick="ew", pady=PADY)
label = ttk.Label(frame, text="Starting MAC")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.mac_var)
entry.grid(row=0, column=1, sticky="ew")
entry.grid(row=0, column=1, sticky=tk.EW)
# draw buttons
frame = ttk.Frame(self.top)
@ -46,9 +46,9 @@ class MacConfigDialog(Dialog):
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_save(self) -> None:
mac = self.mac_var.get()

View file

@ -1,31 +1,29 @@
"""
mobility configuration
"""
import tkinter as tk
from tkinter import ttk
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
from core.gui.wrappers import ConfigOption, Node
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class MobilityConfigDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
def __init__(self, app: "Application", node: Node) -> None:
super().__init__(app, f"{node.name} Mobility Configuration")
self.node: Node = node
self.config_frame: Optional[ConfigFrame] = None
self.has_error: bool = False
try:
config = self.canvas_node.mobility_config
config = self.node.mobility_config
if not config:
config = self.app.core.get_mobility_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
@ -40,22 +38,22 @@ class MobilityConfigDialog(Dialog):
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
self.draw_apply_buttons()
def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_apply(self) -> None:
self.config_frame.parse_config()
self.canvas_node.mobility_config = self.config
self.node.mobility_config = self.config
self.destroy()

View file

@ -1,40 +1,31 @@
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, 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
from core.gui.themes import PADX, PADY
from core.gui.wrappers import MobilityAction, Node
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
ICON_SIZE: int = 16
class MobilityPlayer:
def __init__(
self,
app: "Application",
canvas_node: "CanvasNode",
config: Dict[str, ConfigOption],
) -> None:
def __init__(self, app: "Application", node: Node) -> None:
self.app: "Application" = app
self.canvas_node: "CanvasNode" = canvas_node
self.config: Dict[str, ConfigOption] = config
self.node: Node = node
self.dialog: Optional[MobilityPlayerDialog] = None
self.state: Optional[MobilityAction] = None
def show(self) -> None:
if self.dialog:
self.dialog.destroy()
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
self.dialog = MobilityPlayerDialog(self.app, self.node)
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
if self.state == MobilityAction.START:
self.set_play()
@ -66,20 +57,11 @@ class MobilityPlayer:
class MobilityPlayerDialog(Dialog):
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
)
def __init__(self, app: "Application", node: Node) -> None:
super().__init__(app, f"{node.name} Mobility Player", modal=False)
self.resizable(False, False)
self.geometry("")
self.canvas_node: "CanvasNode" = canvas_node
self.node: Node = canvas_node.core_node
self.config: Dict[str, ConfigOption] = config
self.node: Node = node
self.play_button: Optional[ttk.Button] = None
self.pause_button: Optional[ttk.Button] = None
self.stop_button: Optional[ttk.Button] = None
@ -87,42 +69,43 @@ class MobilityPlayerDialog(Dialog):
self.draw()
def draw(self) -> None:
config = self.node.mobility_config
self.top.columnconfigure(0, weight=1)
file_name = self.config["file"].value
file_name = config["file"].value
label = ttk.Label(self.top, text=file_name)
label.grid(sticky="ew", pady=PADY)
label.grid(sticky=tk.EW, pady=PADY)
self.progressbar = ttk.Progressbar(self.top, mode="indeterminate")
self.progressbar.grid(sticky="ew", pady=PADY)
self.progressbar.grid(sticky=tk.EW, pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
image = self.app.get_icon(ImageEnum.START, ICON_SIZE)
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
self.play_button.image = image
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE)
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
self.pause_button.image = image
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE)
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
self.stop_button.image = image
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
self.stop_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
loop = tk.IntVar(value=int(self.config["loop"].value == "1"))
loop = tk.IntVar(value=int(config["loop"].value == "1"))
checkbutton = ttk.Checkbutton(
frame, text="Loop?", variable=loop, state=tk.DISABLED
)
checkbutton.grid(row=0, column=3, padx=PADX)
rate = self.config["refresh_ms"].value
rate = config["refresh_ms"].value
label = ttk.Label(frame, text=f"rate {rate} ms")
label.grid(row=0, column=4)
@ -148,30 +131,30 @@ class MobilityPlayerDialog(Dialog):
def click_play(self) -> None:
self.set_play()
session_id = self.app.core.session_id
session_id = self.app.core.session.id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.START
session_id, self.node.id, MobilityAction.START.value
)
except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e)
def click_pause(self) -> None:
self.set_pause()
session_id = self.app.core.session_id
session_id = self.app.core.session.id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.PAUSE
session_id, self.node.id, MobilityAction.PAUSE.value
)
except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e)
def click_stop(self) -> None:
self.set_stop()
session_id = self.app.core.session_id
session_id = self.app.core.session.id
try:
self.app.core.client.mobility_action(
session_id, self.node.id, MobilityAction.STOP
session_id, self.node.id, MobilityAction.STOP.value
)
except grpc.RpcError as e:
self.app.show_grpc_exception("Mobility Error", e)

View file

@ -7,7 +7,6 @@ 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
@ -16,6 +15,7 @@ from core.gui.images import Images
from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll, image_chooser
from core.gui.wrappers import Node
if TYPE_CHECKING:
from core.gui.app import Application
@ -126,12 +126,12 @@ class NodeConfigDialog(Dialog):
# field frame
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1)
# icon field
label = ttk.Label(frame, text="Icon")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
self.image_button = ttk.Button(
frame,
text="Icon",
@ -139,49 +139,49 @@ class NodeConfigDialog(Dialog):
compound=tk.NONE,
command=self.click_icon,
)
self.image_button.grid(row=row, column=1, sticky="ew")
self.image_button.grid(row=row, column=1, sticky=tk.EW)
row += 1
# name field
label = ttk.Label(frame, text="Name")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state)
entry.grid(row=row, column=1, sticky="ew")
entry.grid(row=row, column=1, sticky=tk.EW)
row += 1
# node type field
if NodeUtils.is_model_node(self.node.type):
label = ttk.Label(frame, text="Type")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
combobox = ttk.Combobox(
frame,
textvariable=self.type,
values=list(NodeUtils.NODE_MODELS),
state=combo_state,
)
combobox.grid(row=row, column=1, sticky="ew")
combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1
# container image field
if NodeUtils.is_image_node(self.node.type):
label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
entry.grid(row=row, column=1, sticky="ew")
entry.grid(row=row, column=1, sticky=tk.EW)
row += 1
if NodeUtils.is_container_node(self.node.type):
# server
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Server")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
servers = ["localhost"]
servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox(
frame, textvariable=self.server, values=servers, state=combo_state
)
combobox.grid(row=row, column=1, sticky="ew")
combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1
if NodeUtils.is_rj45_node(self.node.type):
@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog):
ifaces = ListboxScroll(frame)
ifaces.listbox.config(state=state)
ifaces.grid(
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY
)
for inf in sorted(response.ifaces[:]):
ifaces.listbox.insert(tk.END, inf)
@ -206,13 +206,13 @@ class NodeConfigDialog(Dialog):
def draw_ifaces(self) -> None:
notebook = ttk.Notebook(self.top)
notebook.grid(sticky="nsew", pady=PADY)
notebook.grid(sticky=tk.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 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.grid(sticky=tk.NSEW, pady=PADY)
tab.columnconfigure(1, weight=1)
tab.columnconfigure(2, weight=1)
notebook.add(tab, text=iface.name)
@ -226,14 +226,16 @@ class NodeConfigDialog(Dialog):
text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, iface.id),
)
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
button.grid(row=row, sticky=tk.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 iface.mac
mac_state = tk.DISABLED if auto_set else tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set)
mac_state = tk.DISABLED if auto_set else tk.NORMAL
if state == tk.DISABLED:
mac_state = tk.DISABLED
checkbutton = ttk.Checkbutton(
tab, text="Auto?", variable=is_auto, state=state
)
@ -241,7 +243,7 @@ class NodeConfigDialog(Dialog):
checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=iface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
entry.grid(row=row, column=2, sticky="ew")
entry.grid(row=row, column=2, sticky=tk.EW)
func = partial(mac_auto, is_auto, entry, mac)
checkbutton.config(command=func)
row += 1
@ -253,7 +255,7 @@ class NodeConfigDialog(Dialog):
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")
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
row += 1
label = ttk.Label(tab, text="IPv6")
@ -263,21 +265,21 @@ class NodeConfigDialog(Dialog):
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")
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_emane_config(self, emane_model: str, iface_id: int) -> None:
dialog = EmaneModelDialog(

View file

@ -10,25 +10,24 @@ from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll
from core.gui.wrappers import Node
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeConfigServiceDialog(Dialog):
def __init__(
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
self, app: "Application", node: Node, services: Set[str] = None
) -> None:
title = f"{canvas_node.core_node.name} Config Services"
title = f"{node.name} Config Services"
super().__init__(app, title)
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = canvas_node.core_node.id
self.node: Node = node
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)
services = set(node.config_services)
self.current_services: Set[str] = services
self.draw()
@ -42,32 +41,32 @@ class NodeConfigServiceDialog(Dialog):
for i in range(3):
frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
label_frame.grid(row=0, column=0, sticky="nsew")
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew")
self.groups.grid(sticky=tk.NSEW)
for group in sorted(self.app.core.config_services_groups):
self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services")
label_frame.grid(row=0, column=1, sticky="nsew")
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
)
self.services.grid(sticky="nsew")
self.services.grid(sticky=tk.NSEW)
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
self.current.grid(sticky=tk.NSEW)
self.draw_current_services()
frame = ttk.Frame(self.top)
@ -75,13 +74,13 @@ class NodeConfigServiceDialog(Dialog):
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Configure", command=self.click_configure)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.click_remove)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
# trigger group change
self.handle_group_change()
@ -102,7 +101,7 @@ class NodeConfigServiceDialog(Dialog):
elif not var.get() and name in self.current_services:
self.current_services.remove(name)
self.draw_current_services()
self.canvas_node.core_node.config_services[:] = self.current_services
self.node.config_services = self.current_services.copy()
def click_configure(self) -> None:
current_selection = self.current.listbox.curselection()
@ -111,8 +110,7 @@ class NodeConfigServiceDialog(Dialog):
self,
self.app,
self.current.listbox.get(current_selection[0]),
self.canvas_node,
self.node_id,
self.node,
)
if not dialog.has_error:
dialog.show()
@ -132,10 +130,8 @@ class NodeConfigServiceDialog(Dialog):
self.current.listbox.itemconfig(tk.END, bg="green")
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.node.config_services = self.current_services.copy()
logging.info("saved node config services: %s", self.node.config_services)
self.destroy()
def click_cancel(self) -> None:
@ -154,4 +150,4 @@ class NodeConfigServiceDialog(Dialog):
return
def is_custom_service(self, service: str) -> bool:
return service in self.canvas_node.config_service_configs
return service in self.node.config_service_configs

View file

@ -9,22 +9,21 @@ from core.gui.dialogs.dialog import Dialog
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll
from core.gui.wrappers import Node
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeServiceDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
title = f"{canvas_node.core_node.name} Services"
def __init__(self, app: "Application", node: Node) -> None:
title = f"{node.name} Services"
super().__init__(app, title)
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = canvas_node.core_node.id
self.node: Node = node
self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services)
services = set(node.services)
self.current_services: Set[str] = services
self.draw()
@ -38,31 +37,31 @@ class NodeServiceDialog(Dialog):
for i in range(3):
frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
label_frame.grid(row=0, column=0, sticky="nsew")
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew")
self.groups.grid(sticky=tk.NSEW)
for group in sorted(self.app.core.services):
self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services")
label_frame.grid(row=0, column=1, sticky="nsew")
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
)
self.services.grid(sticky="nsew")
self.services.grid(sticky=tk.NSEW)
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
label_frame.grid(row=0, column=2, sticky="nsew")
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
label_frame.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew")
self.current.grid(sticky=tk.NSEW)
for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service)
if self.is_custom_service(service):
@ -73,13 +72,13 @@ class NodeServiceDialog(Dialog):
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Configure", command=self.click_configure)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.click_remove)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
# trigger group change
self.handle_group_change()
@ -104,7 +103,7 @@ class NodeServiceDialog(Dialog):
self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green")
self.canvas_node.core_node.services[:] = self.current_services
self.node.services = self.current_services.copy()
def click_configure(self) -> None:
current_selection = self.current.listbox.curselection()
@ -113,8 +112,7 @@ class NodeServiceDialog(Dialog):
self,
self.app,
self.current.listbox.get(current_selection[0]),
self.canvas_node,
self.node_id,
self.node,
)
# if error occurred when creating ServiceConfigDialog, don't show the dialog
@ -128,8 +126,7 @@ class NodeServiceDialog(Dialog):
)
def click_save(self) -> None:
core_node = self.canvas_node.core_node
core_node.services[:] = self.current_services
self.node.services = self.current_services.copy()
self.destroy()
def click_remove(self) -> None:
@ -144,6 +141,6 @@ class NodeServiceDialog(Dialog):
return
def is_custom_service(self, service: str) -> bool:
has_service_config = service in self.canvas_node.service_configs
has_file_config = service in self.canvas_node.service_file_configs
has_service_config = service in self.node.service_configs
has_file_config = service in self.node.service_file_configs
return has_service_config or has_file_config

View file

@ -33,60 +33,60 @@ class ObserverDialog(Dialog):
def draw_listbox(self) -> None:
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(sticky="nsew", pady=PADY)
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
listbox_scroll.columnconfigure(0, weight=1)
listbox_scroll.rowconfigure(0, weight=1)
self.observers = listbox_scroll.listbox
self.observers.grid(row=0, column=0, sticky="nsew")
self.observers.grid(row=0, column=0, sticky=tk.NSEW)
self.observers.bind("<<ListboxSelect>>", self.handle_observer_change)
for name in sorted(self.app.core.custom_observers):
self.observers.insert(tk.END, name)
def draw_form_fields(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew")
entry.grid(row=0, column=1, sticky=tk.EW)
label = ttk.Label(frame, text="Command")
label.grid(row=1, column=0, sticky="w", padx=PADX)
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.cmd)
entry.grid(row=1, column=1, sticky="ew")
entry.grid(row=1, column=1, sticky=tk.EW)
def draw_config_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
self.save_button = ttk.Button(
frame, text="Save", state=tk.DISABLED, command=self.click_save
)
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save_config)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_save_config(self) -> None:
self.app.guiconfig.observers.clear()

View file

@ -34,42 +34,42 @@ class PreferencesDialog(Dialog):
def draw_preferences(self) -> None:
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Theme")
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w")
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky=tk.W)
themes = self.app.style.theme_names()
combobox = ttk.Combobox(
frame, textvariable=self.theme, values=themes, state="readonly"
)
combobox.set(self.theme.get())
combobox.grid(row=0, column=1, sticky="ew")
combobox.grid(row=0, column=1, sticky=tk.EW)
combobox.bind("<<ComboboxSelected>>", self.theme_change)
label = ttk.Label(frame, text="Editor")
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w")
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky=tk.W)
combobox = ttk.Combobox(
frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly"
)
combobox.grid(row=1, column=1, sticky="ew")
combobox.grid(row=1, column=1, sticky=tk.EW)
label = ttk.Label(frame, text="Terminal")
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky=tk.W)
terminals = sorted(appconfig.TERMINALS.values())
combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals)
combobox.grid(row=2, column=1, sticky="ew")
combobox.grid(row=2, column=1, sticky=tk.EW)
label = ttk.Label(frame, text="3D GUI")
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w")
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky=tk.W)
entry = ttk.Entry(frame, textvariable=self.gui3d)
entry.grid(row=3, column=1, sticky="ew")
entry.grid(row=3, column=1, sticky=tk.EW)
label = ttk.Label(frame, text="Scaling")
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky=tk.W)
scale_frame = ttk.Frame(frame)
scale_frame.grid(row=4, column=1, sticky="ew")
scale_frame.grid(row=4, column=1, sticky=tk.EW)
scale_frame.columnconfigure(0, weight=1)
scale = ttk.Scale(
scale_frame,
@ -79,7 +79,7 @@ class PreferencesDialog(Dialog):
orient=tk.HORIZONTAL,
variable=self.gui_scale,
)
scale.grid(row=0, column=0, sticky="ew")
scale.grid(row=0, column=0, sticky=tk.EW)
entry = validation.AppScaleEntry(
scale_frame, textvariable=self.gui_scale, width=4
)
@ -90,15 +90,15 @@ class PreferencesDialog(Dialog):
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def theme_change(self, event: tk.Event) -> None:
theme = self.theme.get()

View file

@ -25,9 +25,9 @@ class RunToolDialog(Dialog):
"""
store all CORE nodes (nodes that execute commands) from all existing nodes
"""
for nid, node in self.app.core.canvas_nodes.items():
if NodeUtils.is_container_node(node.core_node.type):
self.executable_nodes[node.core_node.name] = nid
for node in self.app.core.session.nodes.values():
if NodeUtils.is_container_node(node.type):
self.executable_nodes[node.name] = node.id
def draw(self) -> None:
self.top.rowconfigure(0, weight=1)
@ -38,56 +38,56 @@ class RunToolDialog(Dialog):
def draw_command_frame(self) -> None:
# the main frame
frame = ttk.Frame(self.top)
frame.grid(row=0, column=0, sticky="nsew", padx=PADX)
frame.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD)
labeled_frame.grid(sticky="ew", pady=PADY)
labeled_frame.grid(sticky=tk.EW, pady=PADY)
labeled_frame.rowconfigure(0, weight=1)
labeled_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(labeled_frame, textvariable=self.cmd)
entry.grid(sticky="ew")
entry.grid(sticky=tk.EW)
# results frame
labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD)
labeled_frame.grid(sticky="nsew", pady=PADY)
labeled_frame.grid(sticky=tk.NSEW, pady=PADY)
labeled_frame.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1)
self.result = CodeText(labeled_frame)
self.result.text.config(state=tk.DISABLED, height=15)
self.result.grid(sticky="nsew", pady=PADY)
self.result.grid(sticky=tk.NSEW, pady=PADY)
button_frame = ttk.Frame(labeled_frame)
button_frame.grid(sticky="nsew")
button_frame.grid(sticky=tk.NSEW)
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="Run", command=self.click_run)
button.grid(sticky="ew", padx=PADX)
button.grid(sticky=tk.EW, padx=PADX)
button = ttk.Button(button_frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def draw_nodes_frame(self) -> None:
labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
labeled_frame.grid(row=0, column=1, sticky="nsew")
labeled_frame.grid(row=0, column=1, sticky=tk.NSEW)
labeled_frame.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1)
self.node_list = ListboxScroll(labeled_frame)
self.node_list.listbox.config(selectmode=tk.MULTIPLE)
self.node_list.grid(sticky="nsew", pady=PADY)
self.node_list.grid(sticky=tk.NSEW, pady=PADY)
for n in sorted(self.executable_nodes.keys()):
self.node_list.listbox.insert(tk.END, n)
button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD)
button_frame.grid(sticky="nsew")
button_frame.grid(sticky=tk.NSEW)
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="All", command=self.click_all)
button.grid(sticky="nsew", padx=PADX)
button.grid(sticky=tk.NSEW, padx=PADX)
button = ttk.Button(button_frame, text="None", command=self.click_none)
button.grid(row=0, column=1, sticky="nsew")
button.grid(row=0, column=1, sticky=tk.NSEW)
def click_all(self) -> None:
self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1)
@ -107,7 +107,7 @@ class RunToolDialog(Dialog):
node_name = self.node_list.listbox.get(selection)
node_id = self.executable_nodes[node_name]
response = self.app.core.client.node_command(
self.app.core.session_id, node_id, command
self.app.core.session.id, node_id, command
)
self.result.text.insert(
tk.END, f"> {node_name} > {command}:\n{response.output}\n"

View file

@ -37,12 +37,12 @@ class ServersDialog(Dialog):
def draw_servers(self) -> None:
listbox_scroll = ListboxScroll(self.top)
listbox_scroll.grid(pady=PADY, sticky="nsew")
listbox_scroll.grid(pady=PADY, sticky=tk.NSEW)
listbox_scroll.columnconfigure(0, weight=1)
listbox_scroll.rowconfigure(0, weight=1)
self.servers = listbox_scroll.listbox
self.servers.grid(row=0, column=0, sticky="nsew")
self.servers.grid(row=0, column=0, sticky=tk.NSEW)
self.servers.bind("<<ListboxSelect>>", self.handle_server_change)
for server in self.app.core.servers:
@ -50,52 +50,52 @@ class ServersDialog(Dialog):
def draw_server_configuration(self) -> None:
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
frame.grid(pady=PADY, sticky="ew")
frame.grid(pady=PADY, sticky=tk.EW)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
label = ttk.Label(frame, text="Name")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.name)
entry.grid(row=0, column=1, sticky="ew")
entry.grid(row=0, column=1, sticky=tk.EW)
label = ttk.Label(frame, text="Address")
label.grid(row=0, column=2, sticky="w", padx=PADX)
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.address)
entry.grid(row=0, column=3, sticky="ew")
entry.grid(row=0, column=3, sticky=tk.EW)
def draw_servers_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(pady=PADY, sticky="ew")
frame.grid(pady=PADY, sticky=tk.EW)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Create", command=self.click_create)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
self.save_button = ttk.Button(
frame, text="Save", state=tk.DISABLED, command=self.click_save
)
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
self.delete_button = ttk.Button(
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
)
self.delete_button.grid(row=0, column=2, sticky="ew")
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
def draw_apply_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(
frame, text="Save Configuration", command=self.click_save_configuration
)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_save_configuration(self):
self.app.guiconfig.servers.clear()

View file

@ -7,16 +7,15 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
import grpc
from PIL.ImageTk import PhotoImage
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
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CodeText, ListboxScroll
from core.gui.wrappers import Node, NodeServiceData, ServiceValidationMode
if TYPE_CHECKING:
from core.gui.app import Application
from core.gui.graph.node import CanvasNode
from core.gui.coreclient import CoreClient
ICON_SIZE: int = 16
@ -24,18 +23,12 @@ ICON_SIZE: int = 16
class ServiceConfigDialog(Dialog):
def __init__(
self,
master: tk.BaseWidget,
app: "Application",
service_name: str,
canvas_node: "CanvasNode",
node_id: int,
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
) -> None:
title = f"{service_name} Service"
super().__init__(app, title, master=master)
self.core: "CoreClient" = app.core
self.canvas_node: "CanvasNode" = canvas_node
self.node_id: int = node_id
self.node: Node = node
self.service_name: str = service_name
self.radiovar: tk.IntVar = tk.IntVar(value=2)
self.metadata: str = ""
@ -72,7 +65,7 @@ class ServiceConfigDialog(Dialog):
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.default_config: Optional[NodeServiceData] = None
self.temp_service_files: Dict[str, str] = {}
self.modified_files: Set[str] = set()
self.has_error: bool = False
@ -84,15 +77,13 @@ class ServiceConfigDialog(Dialog):
try:
self.app.core.create_nodes_and_links()
default_config = self.app.core.get_node_service(
self.node_id, self.service_name
self.node.id, self.service_name
)
self.default_startup = default_config.startup[:]
self.default_validate = default_config.validate[:]
self.default_shutdown = default_config.shutdown[:]
self.default_directories = default_config.dirs[:]
custom_service_config = self.canvas_node.service_configs.get(
self.service_name
)
custom_service_config = self.node.service_configs.get(self.service_name)
self.default_config = default_config
service_config = (
custom_service_config if custom_service_config else default_config
@ -109,15 +100,13 @@ class ServiceConfigDialog(Dialog):
self.temp_directories = service_config.dirs[:]
self.original_service_files = {
x: self.app.core.get_node_service_file(
self.node_id, self.service_name, x
self.node.id, self.service_name, x
)
for x in default_config.configs
}
self.temp_service_files = dict(self.original_service_files)
file_configs = self.canvas_node.service_file_configs.get(
self.service_name, {}
)
file_configs = self.node.service_file_configs.get(self.service_name, {})
for file, data in file_configs.items():
self.temp_service_files[file] = data
except grpc.RpcError as e:
@ -130,16 +119,16 @@ class ServiceConfigDialog(Dialog):
# draw metadata
frame = ttk.Frame(self.top)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Meta-data")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata)
self.metadata_entry.grid(row=0, column=1, sticky="ew")
self.metadata_entry.grid(row=0, column=1, sticky=tk.EW)
# draw notebook
self.notebook = ttk.Notebook(self.top)
self.notebook.grid(sticky="nsew", pady=PADY)
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
self.draw_tab_files()
self.draw_tab_directories()
self.draw_tab_startstop()
@ -149,7 +138,7 @@ class ServiceConfigDialog(Dialog):
def draw_tab_files(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Files")
@ -159,15 +148,15 @@ class ServiceConfigDialog(Dialog):
label.grid()
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="File Name")
label.grid(row=0, column=0, padx=PADX, sticky="w")
label.grid(row=0, column=0, padx=PADX, sticky=tk.W)
self.filename_combobox = ttk.Combobox(frame, values=self.filenames)
self.filename_combobox.bind(
"<<ComboboxSelected>>", self.display_service_file_data
)
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
self.filename_combobox.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(
frame, image=self.documentnew_img, command=self.add_filename
)
@ -178,7 +167,7 @@ class ServiceConfigDialog(Dialog):
button.grid(row=0, column=3)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
button = ttk.Radiobutton(
frame,
@ -187,16 +176,16 @@ class ServiceConfigDialog(Dialog):
value=1,
state=tk.DISABLED,
)
button.grid(row=0, column=0, sticky="w", padx=PADX)
button.grid(row=0, column=0, sticky=tk.W, padx=PADX)
entry = ttk.Entry(frame, state=tk.DISABLED)
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
image = Images.get(ImageEnum.FILEOPEN, 16)
button = ttk.Button(frame, image=image)
button.image = image
button.grid(row=0, column=2)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(0, weight=1)
button = ttk.Radiobutton(
frame,
@ -204,7 +193,7 @@ class ServiceConfigDialog(Dialog):
text="Use text below for file contents",
value=2,
)
button.grid(row=0, column=0, sticky="ew")
button.grid(row=0, column=0, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
button = ttk.Button(frame, image=image)
button.image = image
@ -215,7 +204,7 @@ class ServiceConfigDialog(Dialog):
button.grid(row=0, column=2)
self.service_file_data = CodeText(tab)
self.service_file_data.grid(sticky="nsew")
self.service_file_data.grid(sticky=tk.NSEW)
tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1)
if len(self.filenames) > 0:
self.filename_combobox.current(0)
@ -229,7 +218,7 @@ class ServiceConfigDialog(Dialog):
def draw_tab_directories(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
tab.rowconfigure(2, weight=1)
self.notebook.add(tab, text="Directories")
@ -238,33 +227,33 @@ class ServiceConfigDialog(Dialog):
tab,
text="Directories required by this service that are unique for each node.",
)
label.grid(row=0, column=0, sticky="ew")
label.grid(row=0, column=0, sticky=tk.EW)
frame = ttk.Frame(tab, padding=FRAME_PAD)
frame.columnconfigure(0, weight=1)
frame.grid(row=1, column=0, sticky="nsew")
frame.grid(row=1, column=0, sticky=tk.NSEW)
var = tk.StringVar(value="")
self.directory_entry = ttk.Entry(frame, textvariable=var)
self.directory_entry.grid(row=0, column=0, sticky="ew", padx=PADX)
self.directory_entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="...", command=self.find_directory_button)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
self.dir_list = ListboxScroll(tab)
self.dir_list.grid(row=2, column=0, sticky="nsew", pady=PADY)
self.dir_list.grid(row=2, column=0, sticky=tk.NSEW, pady=PADY)
self.dir_list.listbox.bind("<<ListboxSelect>>", self.directory_select)
for d in self.temp_directories:
self.dir_list.listbox.insert("end", d)
frame = ttk.Frame(tab)
frame.grid(row=3, column=0, sticky="nsew")
frame.grid(row=3, column=0, sticky=tk.NSEW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Add", command=self.add_directory)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def draw_tab_startstop(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
for i in range(3):
tab.rowconfigure(i, weight=1)
@ -290,25 +279,25 @@ class ServiceConfigDialog(Dialog):
commands = self.validation_commands
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(1, weight=1)
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
frame = ttk.Frame(label_frame)
frame.grid(row=0, column=0, sticky="nsew", pady=PADY)
frame.grid(row=0, column=0, sticky=tk.NSEW, pady=PADY)
frame.columnconfigure(0, weight=1)
entry = ttk.Entry(frame, textvariable=tk.StringVar())
entry.grid(row=0, column=0, stick="ew", padx=PADX)
button = ttk.Button(frame, image=self.documentnew_img)
button.bind("<Button-1>", self.add_command)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, image=self.editdelete_img)
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
button.bind("<Button-1>", self.delete_command)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.listbox.bind("<<ListboxSelect>>", self.update_entry)
for command in commands:
listbox_scroll.listbox.insert("end", command)
listbox_scroll.listbox.config(height=4)
listbox_scroll.grid(row=1, column=0, sticky="nsew")
listbox_scroll.grid(row=1, column=0, sticky=tk.NSEW)
if i == 0:
self.startup_commands_listbox = listbox_scroll.listbox
elif i == 1:
@ -318,23 +307,23 @@ class ServiceConfigDialog(Dialog):
def draw_tab_configuration(self) -> None:
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew")
tab.grid(sticky=tk.NSEW)
tab.columnconfigure(0, weight=1)
self.notebook.add(tab, text="Configuration", sticky="nsew")
self.notebook.add(tab, text="Configuration", sticky=tk.NSEW)
frame = ttk.Frame(tab)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Validation Time")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
self.validation_time_entry = ttk.Entry(frame)
self.validation_time_entry.insert("end", self.validation_time)
self.validation_time_entry.config(state=tk.DISABLED)
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Validation Mode")
label.grid(row=1, column=0, sticky="w", padx=PADX)
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
if self.validation_mode == ServiceValidationMode.BLOCKING:
mode = "BLOCKING"
elif self.validation_mode == ServiceValidationMode.NON_BLOCKING:
@ -346,48 +335,48 @@ class ServiceConfigDialog(Dialog):
)
self.validation_mode_entry.insert("end", mode)
self.validation_mode_entry.config(state=tk.DISABLED)
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Validation Period")
label.grid(row=2, column=0, sticky="w", padx=PADX)
label.grid(row=2, column=0, sticky=tk.W, padx=PADX)
self.validation_period_entry = ttk.Entry(
frame, state=tk.DISABLED, textvariable=tk.StringVar()
)
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY)
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.grid(sticky=tk.NSEW, pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
listbox_scroll.grid(sticky=tk.NSEW)
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for executable in self.executables:
listbox_scroll.listbox.insert("end", executable)
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
label_frame.grid(sticky="nsew", pady=PADY)
label_frame.grid(sticky=tk.NSEW, pady=PADY)
label_frame.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1)
listbox_scroll = ListboxScroll(label_frame)
listbox_scroll.grid(sticky="nsew")
listbox_scroll.grid(sticky=tk.NSEW)
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
for dependency in self.dependencies:
listbox_scroll.listbox.insert("end", dependency)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(4):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
button.grid(row=0, column=1, sticky="ew", padx=PADX)
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
button.grid(row=0, column=2, sticky="ew", padx=PADX)
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=3, sticky="ew")
button.grid(row=0, column=3, sticky=tk.EW)
def add_filename(self) -> None:
filename = self.filename_combobox.get()
@ -453,7 +442,7 @@ class ServiceConfigDialog(Dialog):
and not self.has_new_files()
and not self.is_custom_directory()
):
self.canvas_node.service_configs.pop(self.service_name, None)
self.node.service_configs.pop(self.service_name, None)
self.current_service_color("")
self.destroy()
return
@ -466,7 +455,7 @@ class ServiceConfigDialog(Dialog):
):
startup, validate, shutdown = self.get_commands()
config = self.core.set_node_service(
self.node_id,
self.node.id,
self.service_name,
dirs=self.temp_directories,
files=list(self.filename_combobox["values"]),
@ -474,15 +463,15 @@ class ServiceConfigDialog(Dialog):
validations=validate,
shutdowns=shutdown,
)
self.canvas_node.service_configs[self.service_name] = config
self.node.service_configs[self.service_name] = config
for file in self.modified_files:
file_configs = self.canvas_node.service_file_configs.setdefault(
file_configs = self.node.service_file_configs.setdefault(
self.service_name, {}
)
file_configs[file] = self.temp_service_files[file]
# TODO: check if this is really needed
self.app.core.set_node_service_file(
self.node_id, self.service_name, file, self.temp_service_files[file]
self.node.id, self.service_name, file, self.temp_service_files[file]
)
self.current_service_color("green")
except grpc.RpcError as e:
@ -526,8 +515,8 @@ class ServiceConfigDialog(Dialog):
clears out any custom configuration permanently
"""
# clear coreclient data
self.canvas_node.service_configs.pop(self.service_name, None)
file_configs = self.canvas_node.service_file_configs.pop(self.service_name, {})
self.node.service_configs.pop(self.service_name, None)
file_configs = self.node.service_file_configs.pop(self.service_name, {})
file_configs.pop(self.service_name, None)
self.temp_service_files = dict(self.original_service_files)
self.modified_files.clear()
@ -564,9 +553,8 @@ class ServiceConfigDialog(Dialog):
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
self.app, self, self.node.name, self.service_name, file_name
)
dialog.show()

View file

@ -5,10 +5,10 @@ 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
from core.gui.wrappers import ConfigOption
if TYPE_CHECKING:
from core.gui.app import Application
@ -26,9 +26,9 @@ class SessionOptionsDialog(Dialog):
def get_config(self) -> Dict[str, ConfigOption]:
try:
session_id = self.app.core.session_id
session_id = self.app.core.session.id
response = self.app.core.client.get_session_options(session_id)
return response.config
return ConfigOption.from_dict(response.config)
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Session Options Error", e)
self.has_error = True
@ -39,22 +39,22 @@ class SessionOptionsDialog(Dialog):
self.top.rowconfigure(0, weight=1)
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.config_frame.grid(sticky=tk.NSEW, pady=PADY)
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
state = tk.NORMAL if self.enabled else tk.DISABLED
button = ttk.Button(frame, text="Save", command=self.save, state=state)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def save(self) -> None:
config = self.config_frame.parse_config()
try:
session_id = self.app.core.session_id
session_id = self.app.core.session.id
response = self.app.core.client.set_session_options(session_id, config)
logging.info("saved session config: %s", response)
except grpc.RpcError as e:

View file

@ -5,12 +5,11 @@ 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
from core.gui.themes import PADX, PADY
from core.gui.wrappers import SessionState, SessionSummary
if TYPE_CHECKING:
from core.gui.app import Application
@ -33,7 +32,7 @@ class SessionsDialog(Dialog):
try:
response = self.app.core.client.get_sessions()
logging.info("sessions: %s", response)
return response.sessions
return [SessionSummary.from_proto(x) for x in response.sessions]
except grpc.RpcError as e:
self.app.show_grpc_exception("Get Sessions Error", e)
self.destroy()
@ -63,7 +62,7 @@ class SessionsDialog(Dialog):
frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
self.tree = ttk.Treeview(
frame,
columns=("id", "state", "nodes"),
@ -73,7 +72,7 @@ class SessionsDialog(Dialog):
style = ttk.Style()
heading_size = int(self.app.guiconfig.scale * 10)
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
self.tree.grid(sticky="nsew")
self.tree.grid(sticky=tk.NSEW)
self.tree.column("id", stretch=tk.YES, anchor="center")
self.tree.heading("id", text="ID")
self.tree.column("state", stretch=tk.YES, anchor="center")
@ -82,7 +81,7 @@ class SessionsDialog(Dialog):
self.tree.heading("nodes", text="Node Count")
for index, session in enumerate(self.sessions):
state_name = core_pb2.SessionState.Enum.Name(session.state)
state_name = SessionState(session.state).name
self.tree.insert(
"",
tk.END,
@ -93,25 +92,25 @@ class SessionsDialog(Dialog):
self.tree.bind("<<TreeviewSelect>>", self.click_select)
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
xscrollbar.grid(row=1, sticky="ew")
xscrollbar.grid(row=1, sticky=tk.EW)
self.tree.configure(xscrollcommand=xscrollbar.set)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
for i in range(4):
frame.columnconfigure(i, weight=1)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
b = ttk.Button(
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
)
b.image = image
b.grid(row=0, padx=PADX, sticky="ew")
b.grid(row=0, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.FILEOPEN, 16)
self.connect_button = ttk.Button(
@ -123,7 +122,7 @@ class SessionsDialog(Dialog):
state=tk.DISABLED,
)
self.connect_button.image = image
self.connect_button.grid(row=0, column=1, padx=PADX, sticky="ew")
self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.DELETE, 16)
self.delete_button = ttk.Button(
@ -135,7 +134,7 @@ class SessionsDialog(Dialog):
state=tk.DISABLED,
)
self.delete_button.image = image
self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew")
self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW)
image = Images.get(ImageEnum.CANCEL, 16)
if self.is_start_app:
@ -155,7 +154,7 @@ class SessionsDialog(Dialog):
command=self.destroy,
)
b.image = image
b.grid(row=0, column=3, sticky="ew")
b.grid(row=0, column=3, sticky=tk.EW)
def click_new(self) -> None:
self.app.core.create_new_session()
@ -182,8 +181,6 @@ class SessionsDialog(Dialog):
def join_session(self, session_id: int) -> None:
self.destroy()
if self.app.core.xml_file:
self.app.core.xml_file = None
task = ProgressTask(
self.app, "Join", self.app.core.join_session, args=(session_id,)
)
@ -191,7 +188,7 @@ class SessionsDialog(Dialog):
def double_click_join(self, _event: tk.Event) -> None:
item = self.tree.selection()
if item is None:
if not item:
return
session_id = int(self.tree.item(item, "text"))
self.join_session(session_id)
@ -202,7 +199,7 @@ class SessionsDialog(Dialog):
logging.debug("delete session: %s", self.selected_session)
self.tree.delete(self.selected_id)
self.app.core.delete_session(self.selected_session)
if self.selected_session == self.app.core.session_id:
if self.selected_session == self.app.core.session.id:
self.click_new()
self.destroy()
self.click_select()

View file

@ -57,15 +57,15 @@ class ShapeDialog(Dialog):
def draw_label_options(self) -> None:
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
label_frame.grid(sticky="ew")
label_frame.grid(sticky=tk.EW)
label_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(label_frame, textvariable=self.shape_text)
entry.grid(sticky="ew", pady=PADY)
entry.grid(sticky=tk.EW, pady=PADY)
# font options
frame = ttk.Frame(label_frame)
frame.grid(sticky="nsew", pady=PADY)
frame.grid(sticky=tk.NSEW, pady=PADY)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
@ -75,70 +75,70 @@ class ShapeDialog(Dialog):
values=sorted(font.families()),
state="readonly",
)
combobox.grid(row=0, column=0, sticky="nsew")
combobox.grid(row=0, column=0, sticky=tk.NSEW)
combobox = ttk.Combobox(
frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly"
)
combobox.grid(row=0, column=1, padx=PADX, sticky="nsew")
combobox.grid(row=0, column=1, padx=PADX, sticky=tk.NSEW)
button = ttk.Button(frame, text="Color", command=self.choose_text_color)
button.grid(row=0, column=2, sticky="nsew")
button.grid(row=0, column=2, sticky=tk.NSEW)
# style options
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(3):
frame.columnconfigure(i, weight=1)
button = ttk.Checkbutton(frame, variable=self.bold, text="Bold")
button.grid(row=0, column=0, sticky="ew")
button.grid(row=0, column=0, sticky=tk.EW)
button = ttk.Checkbutton(frame, variable=self.italic, text="Italic")
button.grid(row=0, column=1, padx=PADX, sticky="ew")
button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
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.grid(sticky=tk.EW, pady=PADY)
label_frame.columnconfigure(0, weight=1)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(1, 3):
frame.columnconfigure(i, weight=1)
label = ttk.Label(frame, text="Fill Color")
label.grid(row=0, column=0, padx=PADX, sticky="w")
label.grid(row=0, column=0, padx=PADX, sticky=tk.W)
self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color)
self.fill.grid(row=0, column=1, sticky="ew", padx=PADX)
self.fill.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Color", command=self.choose_fill_color)
button.grid(row=0, column=2, sticky="ew")
button.grid(row=0, column=2, sticky=tk.EW)
label = ttk.Label(frame, text="Border Color")
label.grid(row=1, column=0, sticky="w", padx=PADX)
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
self.border = ttk.Label(
frame, text=self.border_color, background=self.border_color
)
self.border.grid(row=1, column=1, sticky="ew", padx=PADX)
self.border.grid(row=1, column=1, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Color", command=self.choose_border_color)
button.grid(row=1, column=2, sticky="ew")
button.grid(row=1, column=2, sticky=tk.EW)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew", pady=PADY)
frame.grid(sticky=tk.EW, pady=PADY)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Border Width")
label.grid(row=0, column=0, sticky="w", padx=PADX)
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
combobox = ttk.Combobox(
frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly"
)
combobox.grid(row=0, column=1, sticky="nsew")
combobox.grid(row=0, column=1, sticky=tk.NSEW)
def draw_buttons(self) -> None:
frame = ttk.Frame(self.top)
frame.grid(sticky="nsew")
frame.grid(sticky=tk.NSEW)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Add shape", command=self.click_add)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.cancel)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def choose_text_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.text_color)

View file

@ -37,25 +37,25 @@ class ThroughputDialog(Dialog):
variable=self.show_throughput,
text="Show Throughput Level On Every Link",
)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
button = ttk.Checkbutton(
self.top,
variable=self.exponential_weight,
text="Use Exponential Weighted Moving Average",
)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
button = ttk.Checkbutton(
self.top, variable=self.transmission, text="Include Transmissions"
)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
button = ttk.Checkbutton(
self.top, variable=self.reception, text="Include Receptions"
)
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
label_frame = ttk.LabelFrame(self.top, text="Link Highlight", padding=FRAME_PAD)
label_frame.columnconfigure(0, weight=1)
label_frame.grid(sticky="ew")
label_frame.grid(sticky=tk.EW)
scale = ttk.Scale(
label_frame,
@ -65,21 +65,21 @@ class ThroughputDialog(Dialog):
orient=tk.HORIZONTAL,
variable=self.threshold,
)
scale.grid(sticky="ew", pady=PADY)
scale.grid(sticky=tk.EW, pady=PADY)
frame = ttk.Frame(label_frame)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Threshold Kbps (0 disabled)")
label.grid(row=0, column=0, sticky="ew", padx=PADX)
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.threshold)
entry.grid(row=0, column=1, sticky="ew", pady=PADY)
entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Width")
label.grid(row=1, column=0, sticky="ew", padx=PADX)
label.grid(row=1, column=0, sticky=tk.EW, padx=PADX)
entry = ttk.Entry(frame, textvariable=self.width)
entry.grid(row=1, column=1, sticky="ew", pady=PADY)
entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
label = ttk.Label(frame, text="Color")
label.grid(row=2, column=0, sticky="ew", padx=PADX)
label.grid(row=2, column=0, sticky=tk.EW, padx=PADX)
self.color_button = tk.Button(
frame,
text=self.color,
@ -87,18 +87,18 @@ class ThroughputDialog(Dialog):
bg=self.color,
highlightthickness=0,
)
self.color_button.grid(row=2, column=1, sticky="ew")
self.color_button.grid(row=2, column=1, sticky=tk.EW)
self.draw_spacer()
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save)
button.grid(row=0, column=0, sticky="ew", padx=PADX)
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_color(self) -> None:
color_picker = ColorPickerDialog(self, self.app, self.color)

View file

@ -1,13 +1,13 @@
import tkinter as tk
from tkinter import ttk
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
from core.gui.wrappers import ConfigOption, Node
if TYPE_CHECKING:
from core.gui.app import Application
@ -30,7 +30,7 @@ class WlanConfigDialog(Dialog):
self.ranges: Dict[int, int] = {}
self.positive_int: int = self.app.master.register(self.validate_and_update)
try:
config = self.canvas_node.wlan_config
config = self.node.wlan_config
if not config:
config = self.app.core.get_wlan_config(self.node.id)
self.config: Dict[str, ConfigOption] = config
@ -55,7 +55,7 @@ class WlanConfigDialog(Dialog):
self.top.rowconfigure(0, weight=1)
self.config_frame = ConfigFrame(self.top, self.app, self.config)
self.config_frame.draw_config()
self.config_frame.grid(sticky="nsew", pady=PADY)
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
self.draw_apply_buttons()
self.top.bind("<Destroy>", self.remove_ranges)
@ -64,7 +64,7 @@ class WlanConfigDialog(Dialog):
create node configuration options
"""
frame = ttk.Frame(self.top)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
for i in range(2):
frame.columnconfigure(i, weight=1)
@ -74,19 +74,19 @@ class WlanConfigDialog(Dialog):
self.range_entry.config(validatecommand=(self.positive_int, "%P"))
button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew")
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew")
button.grid(row=0, column=1, sticky=tk.EW)
def click_apply(self) -> None:
"""
retrieve user's wlan configuration and store the new configuration values
"""
config = self.config_frame.parse_config()
self.canvas_node.wlan_config = self.config
self.node.wlan_config = self.config
if self.app.core.is_runtime():
session_id = self.app.core.session_id
session_id = self.app.core.session.id
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
self.remove_ranges()
self.destroy()

View file

@ -1,9 +1,9 @@
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
from core.gui.wrappers import Interface
if TYPE_CHECKING:
from core.gui.app import Application
@ -34,13 +34,11 @@ class EdgeInfoFrame(InfoFrameBase):
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
src_node = self.app.core.session.nodes[link.node1_id]
dst_node = self.app.core.session.nodes[link.node2_id]
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.add_detail("Source", src_node.name)
iface1 = link.iface1
if iface1:
@ -62,7 +60,7 @@ class EdgeInfoFrame(InfoFrameBase):
ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else ""
frame.add_detail("IP6", ip6)
if link.HasField("options"):
if link.options:
frame.add_separator()
bandwidth = bandwidth_text(options.bandwidth)
frame.add_detail("Bandwidth", bandwidth)
@ -81,9 +79,9 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
def draw(self) -> None:
link = self.edge.link
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
src_canvas_node = self.app.canvas.nodes[self.edge.src]
src_node = src_canvas_node.core_node
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
dst_canvas_node = self.app.canvas.nodes[self.edge.dst]
dst_node = dst_canvas_node.core_node
# find interface for each node connected to network
@ -92,7 +90,7 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
iface2 = get_iface(dst_canvas_node, net_id)
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.add_detail("Source", src_node.name)
if iface1:
mac = iface1.mac if iface1.mac else "auto"

View file

@ -1,8 +1,9 @@
import tkinter as tk
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
from core.gui.wrappers import NodeType
if TYPE_CHECKING:
from core.gui.app import Application
@ -18,7 +19,7 @@ class NodeInfoFrame(InfoFrameBase):
self.columnconfigure(0, weight=1)
node = self.canvas_node.core_node
frame = DetailsFrame(self)
frame.grid(sticky="ew")
frame.grid(sticky=tk.EW)
frame.add_detail("ID", node.id)
frame.add_detail("Name", node.name)
if NodeUtils.is_model_node(node.type):

View file

@ -3,14 +3,13 @@ import math
import tkinter as tk
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
from core.gui.utils import bandwidth_text, delay_jitter_text
from core.gui.wrappers import Interface, Link
if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph
@ -111,7 +110,9 @@ class Edge:
arc_y = (perp_m * arc_x) + b
return arc_x, arc_y
def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
def draw(
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str
) -> None:
arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line(
*src_pos,
@ -121,6 +122,7 @@ class Edge:
tags=self.tag,
width=self.scaled_width(),
fill=self.color,
state=state,
)
def redraw(self) -> None:
@ -250,7 +252,7 @@ class CanvasWirelessEdge(Edge):
self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR
self.color: str = color
self.draw(src_pos, dst_pos)
self.draw(src_pos, dst_pos, self.canvas.show_wireless.state())
if link.label:
self.middle_label_text(link.label)
self.set_binding()
@ -287,7 +289,7 @@ class CanvasEdge(Edge):
self.link: Optional[Link] = None
self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos)
self.draw(src_pos, dst_pos, tk.NORMAL)
self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context()
@ -305,7 +307,7 @@ class CanvasEdge(Edge):
self.link = link
self.draw_labels()
def iface_label(self, iface: core_pb2.Interface) -> str:
def iface_label(self, iface: Interface) -> str:
label = ""
if iface.name and self.canvas.show_iface_names.get():
label = f"{iface.name}"
@ -319,10 +321,10 @@ class CanvasEdge(Edge):
def create_node_labels(self) -> Tuple[str, str]:
label1 = None
if self.link.HasField("iface1"):
if self.link.iface1:
label1 = self.iface_label(self.link.iface1)
label2 = None
if self.link.HasField("iface2"):
if self.link.iface2:
label2 = self.iface_label(self.link.iface2)
return label1, label2
@ -379,6 +381,7 @@ class CanvasEdge(Edge):
def check_wireless(self) -> None:
if self.is_wireless():
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.dtag(self.id, tags.EDGE)
self._check_antenna()
def _check_antenna(self) -> None:
@ -417,22 +420,38 @@ class CanvasEdge(Edge):
dialog.show()
def draw_link_options(self):
if not self.link.options:
return
options = self.link.options
asym_options = None
if self.asymmetric_link and self.asymmetric_link.options:
asym_options = self.asymmetric_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}%")
# bandwidth
if options.bandwidth > 0:
bandwidth_line = bandwidth_text(options.bandwidth)
if asym_options and asym_options.bandwidth > 0:
bandwidth_line += f" / {bandwidth_text(asym_options.bandwidth)}"
lines.append(bandwidth_line)
# delay/jitter
dj_line = delay_jitter_text(options.delay, options.jitter)
if dj_line and asym_options:
asym_dj_line = delay_jitter_text(asym_options.delay, asym_options.jitter)
if asym_dj_line:
dj_line += f" / {asym_dj_line}"
if dj_line:
lines.append(dj_line)
# loss
if options.loss > 0:
loss_line = f"loss={options.loss}%"
if asym_options and asym_options.loss > 0:
loss_line += f" / loss={asym_options.loss}%"
lines.append(loss_line)
# duplicate
if options.dup > 0:
dup_line = f"dup={options.dup}%"
if asym_options and asym_options.dup > 0:
dup_line += f" / dup={asym_options.dup}%"
lines.append(dup_line)
label = "\n".join(lines)
self.middle_label_text(label)

View file

@ -7,14 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from PIL import Image
from PIL.ImageTk import PhotoImage
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 (
@ -30,6 +22,7 @@ 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 NodeDraw, NodeUtils
from core.gui.wrappers import Interface, Link, LinkType, Node, Session, ThroughputsEvent
if TYPE_CHECKING:
from core.gui.app import Application
@ -104,6 +97,8 @@ class CanvasGraph(tk.Canvas):
# drawing related
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_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, 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)
@ -135,11 +130,6 @@ class CanvasGraph(tk.Canvas):
self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: Session) -> None:
"""
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
client.
:param session: session to draw
"""
# reset view options to default state
self.show_node_labels.set(True)
self.show_link_labels.set(True)
@ -251,12 +241,12 @@ class CanvasGraph(tk.Canvas):
dst.edges.add(edge)
self.edges[edge.token] = edge
self.core.links[edge.token] = edge
if link.HasField("iface1"):
if link.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"):
if link.iface2:
iface2 = link.iface2
self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
dst.ifaces[iface2.id] = iface2
@ -274,7 +264,7 @@ class CanvasGraph(tk.Canvas):
edge = self.edges.get(token)
if not edge:
return
edge.link.options.CopyFrom(link.options)
edge.link.options = deepcopy(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
@ -323,10 +313,7 @@ class CanvasGraph(tk.Canvas):
edge.middle_label_text(link.label)
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)
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:
@ -335,24 +322,24 @@ class CanvasGraph(tk.Canvas):
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.core.set_canvas_node(core_node, node)
def draw_session(self, session: Session) -> None:
"""
Draw existing session.
"""
# draw existing nodes
for core_node in session.nodes:
for core_node in session.nodes.values():
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
self.add_core_node(core_node)
# draw existing links
# draw existing links
for link in session.links:
logging.debug("drawing link: %s", link)
canvas_node1 = self.core.canvas_nodes[link.node1_id]
canvas_node2 = self.core.canvas_nodes[link.node2_id]
canvas_node1 = self.core.get_canvas_node(link.node1_id)
canvas_node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link)
else:
@ -555,8 +542,8 @@ class CanvasGraph(tk.Canvas):
shape.delete()
self.selection.clear()
self.core.deleted_graph_nodes(nodes)
self.core.deleted_graph_edges(edges)
self.core.deleted_canvas_nodes(nodes)
self.core.deleted_canvas_edges(edges)
def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete()
@ -575,7 +562,7 @@ class CanvasGraph(tk.Canvas):
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
if dst_wireless:
src_node.delete_antenna()
self.core.deleted_graph_edges([edge])
self.core.deleted_canvas_edges([edge])
def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor:
@ -761,8 +748,8 @@ class CanvasGraph(tk.Canvas):
image_file = self.node_draw.image_file
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE)
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
self.core.canvas_nodes[core_node.id] = node
self.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def width_and_height(self) -> Tuple[int, int]:
"""
@ -939,7 +926,8 @@ class CanvasGraph(tk.Canvas):
# maps original node canvas id to copy node canvas id
copy_map = {}
# the edges that will be copy over
to_copy_edges = []
to_copy_edges = set()
to_copy_ids = {x.id for x in self.to_copy}
for canvas_node in self.to_copy:
core_node = canvas_node.core_node
actual_x = core_node.position.x + 50
@ -951,30 +939,57 @@ class CanvasGraph(tk.Canvas):
if not copy:
continue
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
# copy configurations and services
node.core_node.services[:] = canvas_node.core_node.services
node.core_node.config_services[:] = canvas_node.core_node.config_services
node.emane_model_configs = deepcopy(canvas_node.emane_model_configs)
node.wlan_config = deepcopy(canvas_node.wlan_config)
node.mobility_config = deepcopy(canvas_node.mobility_config)
node.service_configs = deepcopy(canvas_node.service_configs)
node.service_file_configs = deepcopy(canvas_node.service_file_configs)
node.config_service_configs = deepcopy(canvas_node.config_service_configs)
node.core_node.services = core_node.services.copy()
node.core_node.config_services = core_node.config_services.copy()
node.core_node.emane_model_configs = deepcopy(core_node.emane_model_configs)
node.core_node.wlan_config = deepcopy(core_node.wlan_config)
node.core_node.mobility_config = deepcopy(core_node.mobility_config)
node.core_node.service_configs = deepcopy(core_node.service_configs)
node.core_node.service_file_configs = deepcopy(
core_node.service_file_configs
)
node.core_node.config_service_configs = deepcopy(
core_node.config_service_configs
)
copy_map[canvas_node.id] = node.id
self.core.canvas_nodes[copy.id] = node
self.nodes[node.id] = node
self.core.set_canvas_node(copy, node)
for edge in canvas_node.edges:
if edge.src not in self.to_copy or edge.dst not in self.to_copy:
if edge.src not in to_copy_ids or edge.dst not in to_copy_ids:
if canvas_node.id == edge.src:
dst_node = self.nodes[edge.dst]
self.create_edge(node, dst_node)
token = create_edge_token(node.id, dst_node.id)
elif canvas_node.id == edge.dst:
src_node = self.nodes[edge.src]
self.create_edge(src_node, node)
token = create_edge_token(src_node.id, node.id)
copy_edge = self.edges[token]
copy_link = copy_edge.link
iface1_id = copy_link.iface1.id if copy_link.iface1 else None
iface2_id = copy_link.iface2.id if copy_link.iface2 else None
options = edge.link.options
if options:
copy_edge.link.options = deepcopy(options)
if options and options.unidirectional:
asym_iface1 = None
if iface1_id is not None:
asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None
if iface2_id is not None:
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_iface2,
iface2=asym_iface1,
options=deepcopy(edge.asymmetric_link.options),
)
copy_edge.redraw()
else:
to_copy_edges.append(edge)
to_copy_edges.add(edge)
# copy link and link config
for edge in to_copy_edges:
@ -986,30 +1001,26 @@ class CanvasGraph(tk.Canvas):
token = create_edge_token(src_node_copy.id, dst_node_copy.id)
copy_edge = self.edges[token]
copy_link = copy_edge.link
iface1_id = copy_link.iface1.id if copy_link.iface1 else None
iface2_id = copy_link.iface2.id if copy_link.iface2 else None
options = edge.link.options
copy_link.options.CopyFrom(options)
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:
if options:
copy_link.options = deepcopy(options)
if options and options.unidirectional:
asym_iface1 = None
if iface1_id:
if iface1_id is not None:
asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None
if iface2_id:
if iface2_id is not None:
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,
iface1=asym_iface2,
iface2=asym_iface1,
options=deepcopy(edge.asymmetric_link.options),
)
copy_edge.redraw()
self.itemconfig(
copy_edge.id,
width=self.itemcget(edge.id, "width"),

View file

@ -1,15 +1,13 @@
import functools
import logging
import tkinter as tk
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Set
import grpc
from PIL.ImageTk import PhotoImage
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 import nodeutils, themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog
@ -20,8 +18,9 @@ from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum
from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
from core.gui.wrappers import Interface, Node, NodeType
if TYPE_CHECKING:
from core.gui.app import Application
@ -58,15 +57,6 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {}
# possible configurations
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 = tk.Menu(self.canvas)
themes.style_menu(self.context)
@ -217,6 +207,7 @@ class CanvasNode:
self.context.delete(0, tk.END)
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_emane = self.core_node.type == NodeType.EMANE
is_mobility = is_wlan or is_emane
if self.app.core.is_runtime():
self.context.add_command(label="Configure", command=self.show_config)
if is_emane:
@ -227,7 +218,7 @@ class CanvasNode:
self.context.add_command(
label="WLAN Config", command=self.show_wlan_config
)
if is_wlan and self.core_node.id in self.app.core.mobility_players:
if is_mobility and self.core_node.id in self.app.core.mobility_players:
self.context.add_command(
label="Mobility Player", command=self.show_mobility_player
)
@ -246,6 +237,7 @@ class CanvasNode:
self.context.add_command(
label="WLAN Config", command=self.show_wlan_config
)
if is_mobility:
self.context.add_command(
label="Mobility Config", command=self.show_mobility_config
)
@ -301,7 +293,7 @@ class CanvasNode:
dialog.show()
def show_mobility_config(self) -> None:
dialog = MobilityConfigDialog(self.app, self)
dialog = MobilityConfigDialog(self.app, self.core_node)
if not dialog.has_error:
dialog.show()
@ -310,15 +302,15 @@ class CanvasNode:
mobility_player.show()
def show_emane_config(self) -> None:
dialog = EmaneConfigDialog(self.app, self)
dialog = EmaneConfigDialog(self.app, self.core_node)
dialog.show()
def show_services(self) -> None:
dialog = NodeServiceDialog(self.app, self)
dialog = NodeServiceDialog(self.app, self.core_node)
dialog.show()
def show_config_services(self) -> None:
dialog = NodeConfigServiceDialog(self.app, self)
dialog = NodeConfigServiceDialog(self.app, self.core_node)
dialog.show()
def has_emane_link(self, iface_id: int) -> Node:
@ -356,3 +348,11 @@ class CanvasNode:
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
dy = node_y - int(23 * self.app.app_scale) - y
self.canvas.move(antenna_id, dx, dy)
def update_icon(self, icon_path: str) -> None:
if not Path(icon_path).exists():
logging.error(f"node icon does not exist: {icon_path}")
return
self.core_node.icon = icon_path
self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
self.canvas.itemconfig(self.id, image=self.image)

View file

@ -5,8 +5,8 @@ from typing import Dict, Optional, Tuple
from PIL import Image
from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import NodeType
from core.gui.appconfig import LOCAL_ICONS_PATH
from core.gui.wrappers import NodeType
class Images:

View file

@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
import netaddr
from netaddr import EUI, IPNetwork
from core.api.grpc.core_pb2 import Interface, Link, Node
from core.gui.graph.node import CanvasNode
from core.gui.nodeutils import NodeUtils
from core.gui.wrappers import Interface, Link, Node
if TYPE_CHECKING:
from core.gui.app import Application
@ -89,10 +89,10 @@ class InterfaceManager:
remaining_subnets = set()
for edge in self.app.core.links.values():
link = edge.link
if link.HasField("iface1"):
if link.iface1:
subnets = self.get_subnets(link.iface1)
remaining_subnets.add(subnets)
if link.HasField("iface2"):
if link.iface2:
subnets = self.get_subnets(link.iface2)
remaining_subnets.add(subnets)
@ -100,9 +100,9 @@ class InterfaceManager:
# or remove used indexes from subnet
ifaces = []
for link in links:
if link.HasField("iface1"):
if link.iface1:
ifaces.append(link.iface1)
if link.HasField("iface2"):
if link.iface2:
ifaces.append(link.iface2)
for iface in ifaces:
subnets = self.get_subnets(iface)
@ -117,9 +117,9 @@ class InterfaceManager:
def joined(self, links: List[Link]) -> None:
ifaces = []
for link in links:
if link.HasField("iface1"):
if link.iface1:
ifaces.append(link.iface1)
if link.HasField("iface2"):
if link.iface2:
ifaces.append(link.iface2)
# add to used subnets and mark used indexes

View file

@ -6,7 +6,6 @@ from functools import partial
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING, Optional
from core.gui.appconfig import XMLS_PATH
from core.gui.coreclient import CoreClient
from core.gui.dialogs.about import AboutDialog
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
@ -168,6 +167,16 @@ class Menubar(tk.Menu):
command=self.canvas.show_link_labels.click_handler,
variable=self.canvas.show_link_labels,
)
menu.add_checkbutton(
label="Links",
command=self.canvas.show_links.click_handler,
variable=self.canvas.show_links,
)
menu.add_checkbutton(
label="Wireless Links",
command=self.canvas.show_wireless.click_handler,
variable=self.canvas.show_wireless,
)
menu.add_checkbutton(
label="Annotations",
command=self.canvas.show_annotations.click_handler,
@ -265,16 +274,13 @@ class Menubar(tk.Menu):
)
def click_save(self, _event=None) -> None:
xml_file = self.core.xml_file
if xml_file:
self.core.save_xml(xml_file)
if self.core.session.file:
self.core.save_xml()
else:
self.click_save_xml()
def click_save_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
init_dir = self.core.get_xml_dir()
file_path = filedialog.asksaveasfilename(
initialdir=init_dir,
title="Save As",
@ -284,12 +290,9 @@ class Menubar(tk.Menu):
if file_path:
self.add_recent_file_to_gui_config(file_path)
self.core.save_xml(file_path)
self.core.xml_file = file_path
def click_open_xml(self, _event: tk.Event = None) -> None:
init_dir = self.core.xml_dir
if not init_dir:
init_dir = str(XMLS_PATH)
init_dir = self.core.get_xml_dir()
file_path = filedialog.askopenfilename(
initialdir=init_dir,
title="Open",
@ -298,12 +301,10 @@ class Menubar(tk.Menu):
if file_path:
self.open_xml_task(file_path)
def open_xml_task(self, filename: str) -> None:
self.add_recent_file_to_gui_config(filename)
self.core.xml_file = filename
self.core.xml_dir = str(os.path.dirname(filename))
def open_xml_task(self, file_path: str) -> None:
self.add_recent_file_to_gui_config(file_path)
self.prompt_save_running_session()
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,))
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,))
task.start()
def execute_python(self) -> None:
@ -357,7 +358,6 @@ class Menubar(tk.Menu):
def click_new(self) -> None:
self.prompt_save_running_session()
self.core.create_new_session()
self.core.xml_file = None
def click_find(self, _event: tk.Event = None) -> None:
dialog = FindDialog(self.app)

View file

@ -3,9 +3,9 @@ from typing import List, Optional, Set
from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import Node, NodeType
from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.wrappers import Node, NodeType
ICON_SIZE: int = 48
ANTENNA_SIZE: int = 32
@ -17,7 +17,7 @@ class NodeDraw:
self.image: Optional[PhotoImage] = None
self.image_enum: Optional[ImageEnum] = None
self.image_file: Optional[str] = None
self.node_type: NodeType = None
self.node_type: Optional[NodeType] = None
self.model: Optional[str] = None
self.services: Set[str] = set()
self.label: Optional[str] = None
@ -63,10 +63,15 @@ class NodeUtils:
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"}
ANTENNA_ICON: PhotoImage = None
@classmethod
def is_mobility(cls, node: Node) -> bool:
return node.type in cls.MOBILITY_NODES
@classmethod
def is_router_node(cls, node: Node) -> bool:
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES

View file

@ -5,9 +5,9 @@ import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING, List, Optional
from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
from core.gui.dialogs.alerts import AlertsDialog
from core.gui.themes import Styles
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
if TYPE_CHECKING:
from core.gui.app import Application
@ -34,7 +34,7 @@ class StatusBar(ttk.Frame):
self.columnconfigure(3, weight=1)
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
frame.grid(row=0, column=0, sticky="ew")
frame.grid(row=0, column=0, sticky=tk.EW)
frame.columnconfigure(0, weight=1)
self.status = ttk.Label(
@ -44,22 +44,22 @@ class StatusBar(ttk.Frame):
borderwidth=1,
relief=tk.RIDGE,
)
self.status.grid(row=0, column=0, sticky="ew")
self.status.grid(row=0, column=0, sticky=tk.EW)
self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
self.zoom.grid(row=0, column=1, sticky="ew")
self.zoom.grid(row=0, column=1, sticky=tk.EW)
self.set_zoom(self.app.canvas.ratio)
self.cpu_label = ttk.Label(
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE
)
self.cpu_label.grid(row=0, column=2, sticky="ew")
self.cpu_label.grid(row=0, column=2, sticky=tk.EW)
self.set_cpu(0.0)
self.alerts_button = ttk.Button(
self, text="Alerts", command=self.click_alerts, style=self.alert_style
)
self.alerts_button.grid(row=0, column=3, sticky="ew")
self.alerts_button.grid(row=0, column=3, sticky=tk.EW)
def set_cpu(self, usage: float) -> None:
self.cpu_label.config(text=f"CPU {usage * 100:.2f}%")
@ -69,7 +69,7 @@ class StatusBar(ttk.Frame):
def add_alert(self, event: ExceptionEvent) -> None:
self.core_alarms.append(event)
level = event.exception_event.level
level = event.level
self._set_alert_style(level)
label = f"Alerts ({len(self.core_alarms)})"
self.alerts_button.config(text=label, style=self.alert_style)

View file

@ -1,6 +1,7 @@
import logging
import threading
import time
import tkinter as tk
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple
if TYPE_CHECKING:
@ -26,22 +27,20 @@ class ProgressTask:
self.time: Optional[float] = None
def start(self) -> None:
self.app.progress.grid(sticky="ew", columnspan=2)
self.app.progress.grid(sticky=tk.EW, columnspan=2)
self.app.progress.start()
self.time = time.perf_counter()
thread = threading.Thread(target=self.run, daemon=True)
thread.start()
def run(self) -> None:
logging.info("running task")
try:
values = self.task(*self.args)
if values is None:
values = ()
elif values and not isinstance(values, tuple):
elif values is not None and not isinstance(values, tuple):
values = (values,)
if self.callback:
logging.info("calling callback")
self.app.after(0, self.callback, *values)
except Exception as e:
logging.exception("progress task exception")

View file

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Callable, List, Optional
from PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2
from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph import tags
@ -96,7 +95,7 @@ class ButtonBar(ttk.Frame):
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
button = ttk.Button(self, image=image, command=func)
button.image = image
button.grid(sticky="ew")
button.grid(sticky=tk.EW)
Tooltip(button, tooltip)
if radio:
self.radio_buttons.append(button)
@ -125,7 +124,7 @@ class MarkerFrame(ttk.Frame):
image = self.app.get_icon(ImageEnum.DELETE, 16)
button = ttk.Button(self, image=image, width=2, command=self.click_clear)
button.image = image
button.grid(sticky="ew", pady=self.PAD)
button.grid(sticky=tk.EW, pady=self.PAD)
Tooltip(button, "Delete Marker")
sizes = [1, 3, 8, 10]
@ -133,14 +132,14 @@ class MarkerFrame(ttk.Frame):
sizes = ttk.Combobox(
self, state="readonly", textvariable=self.size, value=sizes, width=2
)
sizes.grid(sticky="ew", pady=self.PAD)
sizes.grid(sticky=tk.EW, pady=self.PAD)
Tooltip(sizes, "Marker Size")
frame_size = TOOLBAR_SIZE
self.color_frame = tk.Frame(
self, background=self.color, height=frame_size, width=frame_size
)
self.color_frame.grid(sticky="ew")
self.color_frame.grid(sticky=tk.EW)
self.color_frame.bind("<Button-1>", self.click_color)
Tooltip(self.color_frame, "Marker Color")
@ -208,7 +207,7 @@ class Toolbar(ttk.Frame):
def draw_design_frame(self) -> None:
self.design_frame = ButtonBar(self, self.app)
self.design_frame.grid(row=0, column=0, sticky="nsew")
self.design_frame.grid(row=0, column=0, sticky=tk.NSEW)
self.design_frame.columnconfigure(0, weight=1)
self.play_button = self.design_frame.create_button(
ImageEnum.START, self.click_start, "Start Session"
@ -240,7 +239,7 @@ class Toolbar(ttk.Frame):
def draw_runtime_frame(self) -> None:
self.runtime_frame = ButtonBar(self, self.app)
self.runtime_frame.grid(row=0, column=0, sticky="nsew")
self.runtime_frame.grid(row=0, column=0, sticky=tk.NSEW)
self.runtime_frame.columnconfigure(0, weight=1)
self.stop_button = self.runtime_frame.create_button(
ImageEnum.STOP, self.click_stop, "Stop Session"
@ -300,15 +299,15 @@ class Toolbar(ttk.Frame):
)
task.start()
def start_callback(self, response: core_pb2.StartSessionResponse) -> None:
if response.result:
def start_callback(self, result: bool, exceptions: List[str]) -> None:
if result:
self.set_runtime()
self.app.core.set_metadata()
self.app.core.show_mobility_players()
else:
enable_buttons(self.design_frame, enabled=True)
if response.exceptions:
message = "\n".join(response.exceptions)
if exceptions:
message = "\n".join(exceptions)
self.app.show_error("Start Session Error", message)
def set_runtime(self) -> None:
@ -388,7 +387,7 @@ class Toolbar(ttk.Frame):
self.runtime_frame, image=image, direction=tk.RIGHT
)
menu_button.image = image
menu_button.grid(sticky="ew")
menu_button.grid(sticky=tk.EW)
self.observers_menu = ObserversMenu(menu_button, self.app)
menu_button["menu"] = self.observers_menu
@ -405,7 +404,7 @@ class Toolbar(ttk.Frame):
)
task.start()
def stop_callback(self, response: core_pb2.StopSessionResponse) -> None:
def stop_callback(self, result: bool) -> None:
self.set_design()
self.app.canvas.stopped_session()

View file

@ -46,7 +46,7 @@ class Tooltip(object):
self.tw.rowconfigure(0, weight=1)
self.tw.columnconfigure(0, weight=1)
frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3)
frame.grid(sticky="nsew")
frame.grid(sticky=tk.NSEW)
label = ttk.Label(frame, text=self.text, style=Styles.tooltip)
label.grid()

View file

@ -1,3 +1,6 @@
from typing import Optional
def bandwidth_text(bandwidth: int) -> str:
size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"}
unit = 1000
@ -8,3 +11,12 @@ def bandwidth_text(bandwidth: int) -> str:
if i == 3:
break
return f"{bandwidth} {size[i]}"
def delay_jitter_text(delay: int, jitter: int) -> Optional[str]:
line = None
if delay > 0 and jitter > 0:
line = f"{delay} us (\u00B1{jitter} us)"
elif jitter > 0:
line = f"0 us (\u00B1{jitter} us)"
return line

View file

@ -5,12 +5,10 @@ from pathlib import Path
from tkinter import filedialog, font, ttk
from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type
from core.api.grpc import core_pb2
from core.api.grpc.common_pb2 import ConfigOption
from core.api.grpc.core_pb2 import ConfigOptionType
from core.gui import themes, validation
from core.gui import appconfig, themes, validation
from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.wrappers import ConfigOption, ConfigOptionType
if TYPE_CHECKING:
from core.gui.app import Application
@ -28,7 +26,9 @@ INT_TYPES: Set[ConfigOptionType] = {
def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None:
file_path = filedialog.askopenfilename(title="Select File", parent=parent)
file_path = filedialog.askopenfilename(
title="Select File", initialdir=str(appconfig.HOME_PATH), parent=parent
)
if file_path:
value.set(file_path)
@ -47,13 +47,13 @@ class FrameScroll(ttk.Frame):
self.columnconfigure(0, weight=1)
bg = self.app.style.lookup(".", "background")
self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg)
self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2)
self.canvas.grid(row=0, sticky=tk.NSEW, padx=2, pady=2)
self.canvas.columnconfigure(0, weight=1)
self.canvas.rowconfigure(0, weight=1)
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(
self, orient="vertical", command=self.canvas.yview
)
self.scrollbar.grid(row=0, column=1, sticky="ns")
self.scrollbar.grid(row=0, column=1, sticky=tk.NS)
self.frame: ttk.Frame = _cls(self.canvas)
self.frame_id: int = self.canvas.create_window(
0, 0, anchor="nw", window=self.frame
@ -108,15 +108,15 @@ class ConfigFrame(ttk.Notebook):
self.add(tab, text=group_name)
for index, option in enumerate(sorted(group, key=lambda x: x.name)):
label = ttk.Label(tab.frame, text=option.label)
label.grid(row=index, pady=PADY, padx=PADX, sticky="w")
label.grid(row=index, pady=PADY, padx=PADX, sticky=tk.W)
value = tk.StringVar()
if option.type == core_pb2.ConfigOptionType.BOOL:
if option.type == ConfigOptionType.BOOL:
select = ("On", "Off")
state = "readonly" if self.enabled else tk.DISABLED
combobox = ttk.Combobox(
tab.frame, textvariable=value, values=select, state=state
)
combobox.grid(row=index, column=1, sticky="ew")
combobox.grid(row=index, column=1, sticky=tk.EW)
if option.value == "1":
value.set("On")
else:
@ -128,16 +128,16 @@ class ConfigFrame(ttk.Notebook):
combobox = ttk.Combobox(
tab.frame, textvariable=value, values=select, state=state
)
combobox.grid(row=index, column=1, sticky="ew")
elif option.type == core_pb2.ConfigOptionType.STRING:
combobox.grid(row=index, column=1, sticky=tk.EW)
elif option.type == ConfigOptionType.STRING:
value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED
if "file" in option.label:
file_frame = ttk.Frame(tab.frame)
file_frame.grid(row=index, column=1, sticky="ew")
file_frame.grid(row=index, column=1, sticky=tk.EW)
file_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(file_frame, textvariable=value, state=state)
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
func = partial(file_button_click, value, self)
button = ttk.Button(
file_frame, text="...", command=func, state=state
@ -145,21 +145,21 @@ class ConfigFrame(ttk.Notebook):
button.grid(row=0, column=1)
else:
entry = ttk.Entry(tab.frame, textvariable=value, state=state)
entry.grid(row=index, column=1, sticky="ew")
entry.grid(row=index, column=1, sticky=tk.EW)
elif option.type in INT_TYPES:
value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED
entry = validation.PositiveIntEntry(
tab.frame, textvariable=value, state=state
)
entry.grid(row=index, column=1, sticky="ew")
elif option.type == core_pb2.ConfigOptionType.FLOAT:
entry.grid(row=index, column=1, sticky=tk.EW)
elif option.type == ConfigOptionType.FLOAT:
value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED
entry = validation.PositiveFloatEntry(
tab.frame, textvariable=value, state=state
)
entry.grid(row=index, column=1, sticky="ew")
entry.grid(row=index, column=1, sticky=tk.EW)
else:
logging.error("unhandled config option type: %s", option.type)
self.values[option.name] = value
@ -169,7 +169,7 @@ class ConfigFrame(ttk.Notebook):
option = self.config[key]
value = self.values[key]
config_value = value.get()
if option.type == core_pb2.ConfigOptionType.BOOL:
if option.type == ConfigOptionType.BOOL:
if config_value == "On":
option.value = "1"
else:
@ -182,7 +182,7 @@ class ConfigFrame(ttk.Notebook):
for name, data in config.items():
option = self.config[name]
value = self.values[name]
if option.type == core_pb2.ConfigOptionType.BOOL:
if option.type == ConfigOptionType.BOOL:
if data == "1":
data = "On"
else:
@ -196,7 +196,7 @@ class ListboxScroll(ttk.Frame):
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
self.scrollbar.grid(row=0, column=1, sticky="ns")
self.scrollbar.grid(row=0, column=1, sticky=tk.NS)
self.listbox: tk.Listbox = tk.Listbox(
self,
selectmode=tk.BROWSE,
@ -204,7 +204,7 @@ class ListboxScroll(ttk.Frame):
exportselection=False,
)
themes.style_listbox(self.listbox)
self.listbox.grid(row=0, column=0, sticky="nsew")
self.listbox.grid(row=0, column=0, sticky=tk.NSEW)
self.scrollbar.config(command=self.listbox.yview)
@ -224,7 +224,7 @@ class CheckboxList(FrameScroll):
var = tk.BooleanVar(value=checked)
func = partial(self.clicked, name, var)
checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
checkbox.grid(sticky="w")
checkbox.grid(sticky=tk.W)
class CodeFont(font.Font):
@ -250,9 +250,9 @@ class CodeText(ttk.Frame):
selectforeground="black",
relief=tk.FLAT,
)
self.text.grid(row=0, column=0, sticky="nsew")
self.text.grid(row=0, column=0, sticky=tk.NSEW)
yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
yscrollbar.grid(row=0, column=1, sticky="ns")
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
self.text.configure(yscrollcommand=yscrollbar.set)

662
daemon/core/gui/wrappers.py Normal file
View file

@ -0,0 +1,662 @@
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2
class ConfigServiceValidationMode(Enum):
BLOCKING = 0
NON_BLOCKING = 1
TIMER = 2
class ServiceValidationMode(Enum):
BLOCKING = 0
NON_BLOCKING = 1
TIMER = 2
class MobilityAction(Enum):
START = 0
PAUSE = 1
STOP = 2
class ConfigOptionType(Enum):
UINT8 = 1
UINT16 = 2
UINT32 = 3
UINT64 = 4
INT8 = 5
INT16 = 6
INT32 = 7
INT64 = 8
FLOAT = 9
STRING = 10
BOOL = 11
class SessionState(Enum):
DEFINITION = 1
CONFIGURATION = 2
INSTANTIATION = 3
RUNTIME = 4
DATACOLLECT = 5
SHUTDOWN = 6
class NodeType(Enum):
DEFAULT = 0
PHYSICAL = 1
SWITCH = 4
HUB = 5
WIRELESS_LAN = 6
RJ45 = 7
TUNNEL = 8
EMANE = 10
TAP_BRIDGE = 11
PEER_TO_PEER = 12
CONTROL_NET = 13
DOCKER = 15
LXC = 16
class LinkType(Enum):
WIRELESS = 0
WIRED = 1
class ExceptionLevel(Enum):
DEFAULT = 0
FATAL = 1
ERROR = 2
WARNING = 3
NOTICE = 4
class MessageType(Enum):
NONE = 0
ADD = 1
DELETE = 2
CRI = 4
LOCAL = 8
STRING = 16
TEXT = 32
TTY = 64
@dataclass
class ConfigService:
group: str
name: str
executables: List[str]
dependencies: List[str]
directories: List[str]
files: List[str]
startup: List[str]
validate: List[str]
shutdown: List[str]
validation_mode: ConfigServiceValidationMode
validation_timer: int
validation_period: float
@classmethod
def from_proto(cls, proto: configservices_pb2.ConfigService) -> "ConfigService":
return ConfigService(
group=proto.group,
name=proto.name,
executables=proto.executables,
dependencies=proto.dependencies,
directories=proto.directories,
files=proto.files,
startup=proto.startup,
validate=proto.validate,
shutdown=proto.shutdown,
validation_mode=ConfigServiceValidationMode(proto.validation_mode),
validation_timer=proto.validation_timer,
validation_period=proto.validation_period,
)
@dataclass
class ConfigServiceData:
templates: Dict[str, str] = field(default_factory=dict)
config: Dict[str, str] = field(default_factory=dict)
@dataclass
class NodeServiceData:
executables: List[str]
dependencies: List[str]
dirs: List[str]
configs: List[str]
startup: List[str]
validate: List[str]
validation_mode: ServiceValidationMode
validation_timer: int
shutdown: List[str]
meta: str
@classmethod
def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData":
return NodeServiceData(
executables=proto.executables,
dependencies=proto.dependencies,
dirs=proto.dirs,
configs=proto.configs,
startup=proto.startup,
validate=proto.validate,
validation_mode=proto.validation_mode,
validation_timer=proto.validation_timer,
shutdown=proto.shutdown,
meta=proto.meta,
)
@dataclass
class BridgeThroughput:
node_id: int
throughput: float
@classmethod
def from_proto(cls, proto: core_pb2.BridgeThroughput) -> "BridgeThroughput":
return BridgeThroughput(node_id=proto.node_id, throughput=proto.throughput)
@dataclass
class InterfaceThroughput:
node_id: int
iface_id: int
throughput: float
@classmethod
def from_proto(cls, proto: core_pb2.InterfaceThroughput) -> "InterfaceThroughput":
return InterfaceThroughput(
node_id=proto.node_id, iface_id=proto.iface_id, throughput=proto.throughput
)
@dataclass
class ThroughputsEvent:
session_id: int
bridge_throughputs: List[BridgeThroughput]
iface_throughputs: List[InterfaceThroughput]
@classmethod
def from_proto(cls, proto: core_pb2.ThroughputsEvent) -> "ThroughputsEvent":
bridges = [BridgeThroughput.from_proto(x) for x in proto.bridge_throughputs]
ifaces = [InterfaceThroughput.from_proto(x) for x in proto.iface_throughputs]
return ThroughputsEvent(
session_id=proto.session_id,
bridge_throughputs=bridges,
iface_throughputs=ifaces,
)
@dataclass
class SessionLocation:
x: float
y: float
z: float
lat: float
lon: float
alt: float
scale: float
@classmethod
def from_proto(cls, proto: core_pb2.SessionLocation) -> "SessionLocation":
return SessionLocation(
x=proto.x,
y=proto.y,
z=proto.z,
lat=proto.lat,
lon=proto.lon,
alt=proto.alt,
scale=proto.scale,
)
def to_proto(self) -> core_pb2.SessionLocation:
return core_pb2.SessionLocation(
x=self.x,
y=self.y,
z=self.z,
lat=self.lat,
lon=self.lon,
alt=self.alt,
scale=self.scale,
)
@dataclass
class ExceptionEvent:
session_id: int
node_id: int
level: ExceptionLevel
source: str
date: str
text: str
opaque: str
@classmethod
def from_proto(
cls, session_id: int, proto: core_pb2.ExceptionEvent
) -> "ExceptionEvent":
return ExceptionEvent(
session_id=session_id,
node_id=proto.node_id,
level=ExceptionLevel(proto.level),
source=proto.source,
date=proto.date,
text=proto.text,
opaque=proto.opaque,
)
@dataclass
class ConfigOption:
label: str
name: str
value: str
type: ConfigOptionType
group: str
select: List[str] = None
@classmethod
def from_dict(
cls, config: Dict[str, common_pb2.ConfigOption]
) -> Dict[str, "ConfigOption"]:
d = {}
for key, value in config.items():
d[key] = ConfigOption.from_proto(value)
return d
@classmethod
def to_dict(cls, config: Dict[str, "ConfigOption"]) -> Dict[str, str]:
return {k: v.value for k, v in config.items()}
@classmethod
def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption":
return ConfigOption(
label=proto.label,
name=proto.name,
value=proto.value,
type=ConfigOptionType(proto.type),
group=proto.group,
select=proto.select,
)
@dataclass
class Interface:
id: int
name: str = None
mac: str = None
ip4: str = None
ip4_mask: int = None
ip6: str = None
ip6_mask: int = None
net_id: int = None
flow_id: int = None
mtu: int = None
node_id: int = None
net2_id: int = None
@classmethod
def from_proto(cls, proto: core_pb2.Interface) -> "Interface":
return Interface(
id=proto.id,
name=proto.name,
mac=proto.mac,
ip4=proto.ip4,
ip4_mask=proto.ip4_mask,
ip6=proto.ip6,
ip6_mask=proto.ip6_mask,
net_id=proto.net_id,
flow_id=proto.flow_id,
mtu=proto.mtu,
node_id=proto.node_id,
net2_id=proto.net2_id,
)
def to_proto(self) -> core_pb2.Interface:
return core_pb2.Interface(
id=self.id,
name=self.name,
mac=self.mac,
ip4=self.ip4,
ip4_mask=self.ip4_mask,
ip6=self.ip6,
ip6_mask=self.ip6_mask,
net_id=self.net_id,
flow_id=self.flow_id,
mtu=self.mtu,
node_id=self.node_id,
net2_id=self.net2_id,
)
@dataclass
class LinkOptions:
jitter: int = 0
key: int = 0
mburst: int = 0
mer: int = 0
loss: float = 0.0
bandwidth: int = 0
burst: int = 0
delay: int = 0
dup: int = 0
unidirectional: bool = False
@classmethod
def from_proto(cls, proto: core_pb2.LinkOptions) -> "LinkOptions":
return LinkOptions(
jitter=proto.jitter,
key=proto.key,
mburst=proto.mburst,
mer=proto.mer,
loss=proto.loss,
bandwidth=proto.bandwidth,
burst=proto.burst,
delay=proto.delay,
dup=proto.dup,
unidirectional=proto.unidirectional,
)
def to_proto(self) -> core_pb2.LinkOptions:
return core_pb2.LinkOptions(
jitter=self.jitter,
key=self.key,
mburst=self.mburst,
mer=self.mer,
loss=self.loss,
bandwidth=self.bandwidth,
burst=self.burst,
delay=self.delay,
dup=self.dup,
unidirectional=self.unidirectional,
)
@dataclass
class Link:
node1_id: int
node2_id: int
type: LinkType = LinkType.WIRED
iface1: Interface = None
iface2: Interface = None
options: LinkOptions = None
network_id: int = None
label: str = None
color: str = None
@classmethod
def from_proto(cls, proto: core_pb2.Link) -> "Link":
iface1 = None
if proto.HasField("iface1"):
iface1 = Interface.from_proto(proto.iface1)
iface2 = None
if proto.HasField("iface2"):
iface2 = Interface.from_proto(proto.iface2)
options = None
if proto.HasField("options"):
options = LinkOptions.from_proto(proto.options)
return Link(
type=LinkType(proto.type),
node1_id=proto.node1_id,
node2_id=proto.node2_id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=proto.network_id,
label=proto.label,
color=proto.color,
)
def to_proto(self) -> core_pb2.Link:
iface1 = self.iface1.to_proto() if self.iface1 else None
iface2 = self.iface2.to_proto() if self.iface2 else None
options = self.options.to_proto() if self.options else None
return core_pb2.Link(
type=self.type.value,
node1_id=self.node1_id,
node2_id=self.node2_id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=self.network_id,
label=self.label,
color=self.color,
)
def is_symmetric(self) -> bool:
result = True
if self.options:
result = self.options.unidirectional is False
return result
@dataclass
class SessionSummary:
id: int
state: SessionState
nodes: int
file: str
dir: str
@classmethod
def from_proto(cls, proto: core_pb2.SessionSummary) -> "SessionSummary":
return SessionSummary(
id=proto.id,
state=SessionState(proto.state),
nodes=proto.nodes,
file=proto.file,
dir=proto.dir,
)
@dataclass
class Hook:
state: SessionState
file: str
data: str
@classmethod
def from_proto(cls, proto: core_pb2.Hook) -> "Hook":
return Hook(state=SessionState(proto.state), file=proto.file, data=proto.data)
def to_proto(self) -> core_pb2.Hook:
return core_pb2.Hook(state=self.state.value, file=self.file, data=self.data)
@dataclass
class Position:
x: float
y: float
@classmethod
def from_proto(cls, proto: core_pb2.Position) -> "Position":
return Position(x=proto.x, y=proto.y)
def to_proto(self) -> core_pb2.Position:
return core_pb2.Position(x=self.x, y=self.y)
@dataclass
class Geo:
lat: float = None
lon: float = None
alt: float = None
@classmethod
def from_proto(cls, proto: core_pb2.Geo) -> "Geo":
return Geo(lat=proto.lat, lon=proto.lon, alt=proto.alt)
def to_proto(self) -> core_pb2.Geo:
return core_pb2.Geo(lat=self.lat, lon=self.lon, alt=self.alt)
@dataclass
class Node:
id: int
name: str
type: NodeType
model: str = None
position: Position = None
services: Set[str] = field(default_factory=set)
config_services: Set[str] = field(default_factory=set)
emane: str = None
icon: str = None
image: str = None
server: str = None
geo: Geo = None
dir: str = None
channel: str = None
# configurations
emane_model_configs: Dict[
Tuple[str, Optional[int]], Dict[str, ConfigOption]
] = field(default_factory=dict, repr=False)
wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
service_configs: Dict[str, NodeServiceData] = field(
default_factory=dict, repr=False
)
service_file_configs: Dict[str, Dict[str, str]] = field(
default_factory=dict, repr=False
)
config_service_configs: Dict[str, ConfigServiceData] = field(
default_factory=dict, repr=False
)
@classmethod
def from_proto(cls, proto: core_pb2.Node) -> "Node":
return Node(
id=proto.id,
name=proto.name,
type=NodeType(proto.type),
model=proto.model,
position=Position.from_proto(proto.position),
services=set(proto.services),
config_services=set(proto.config_services),
emane=proto.emane,
icon=proto.icon,
image=proto.image,
server=proto.server,
geo=Geo.from_proto(proto.geo),
dir=proto.dir,
channel=proto.channel,
)
def to_proto(self) -> core_pb2.Node:
return core_pb2.Node(
id=self.id,
name=self.name,
type=self.type.value,
model=self.model,
position=self.position.to_proto(),
services=self.services,
config_services=self.config_services,
emane=self.emane,
icon=self.icon,
image=self.image,
server=self.server,
dir=self.dir,
channel=self.channel,
)
@dataclass
class Session:
id: int
state: SessionState
nodes: Dict[int, Node]
links: List[Link]
dir: str
user: str
default_services: Dict[str, Set[str]]
location: SessionLocation
hooks: Dict[str, Hook]
emane_models: List[str]
emane_config: Dict[str, ConfigOption]
metadata: Dict[str, str]
file: Path
@classmethod
def from_proto(cls, proto: core_pb2.Session) -> "Session":
nodes: Dict[int, Node] = {x.id: Node.from_proto(x) for x in proto.nodes}
links = [Link.from_proto(x) for x in proto.links]
default_services = {
x.node_type: set(x.services) for x in proto.default_services
}
hooks = {x.file: Hook.from_proto(x) for x in proto.hooks}
# update nodes with their current configurations
for model in proto.emane_model_configs:
iface_id = None
if model.iface_id != -1:
iface_id = model.iface_id
node = nodes[model.node_id]
key = (model.model, iface_id)
node.emane_model_configs[key] = ConfigOption.from_dict(model.config)
for node_id, mapped_config in proto.wlan_configs.items():
node = nodes[node_id]
node.wlan_config = ConfigOption.from_dict(mapped_config.config)
for config in proto.service_configs:
service = config.service
node = nodes[config.node_id]
node.service_configs[service] = NodeServiceData.from_proto(config.data)
for file, data in config.files.items():
files = node.service_file_configs.setdefault(service, {})
files[file] = data
for config in proto.config_service_configs:
node = nodes[config.node_id]
node.config_service_configs[config.name] = ConfigServiceData(
templates=dict(config.templates), config=dict(config.config)
)
for node_id, mapped_config in proto.mobility_configs.items():
node = nodes[node_id]
node.mobility_config = ConfigOption.from_dict(mapped_config.config)
file_path = Path(proto.file) if proto.file else None
return Session(
id=proto.id,
state=SessionState(proto.state),
nodes=nodes,
links=links,
dir=proto.dir,
user=proto.user,
default_services=default_services,
location=SessionLocation.from_proto(proto.location),
hooks=hooks,
emane_models=list(proto.emane_models),
emane_config=ConfigOption.from_dict(proto.emane_config),
metadata=dict(proto.metadata),
file=file_path,
)
@dataclass
class LinkEvent:
message_type: MessageType
link: Link
@classmethod
def from_proto(cls, proto: core_pb2.LinkEvent) -> "LinkEvent":
return LinkEvent(
message_type=MessageType(proto.message_type),
link=Link.from_proto(proto.link),
)
@dataclass
class NodeEvent:
message_type: MessageType
node: Node
@classmethod
def from_proto(cls, proto: core_pb2.NodeEvent) -> "NodeEvent":
return NodeEvent(
message_type=MessageType(proto.message_type),
node=Node.from_proto(proto.node),
)

View file

@ -9,10 +9,11 @@ import threading
import time
from functools import total_ordering
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
from core import utils
from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager
from core.emane.nodes import EmaneNet
from core.emulator.data import EventData, LinkData, LinkOptions
from core.emulator.enumerations import (
ConfigDataTypes,
@ -31,6 +32,13 @@ if TYPE_CHECKING:
from core.emulator.session import Session
def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]:
try:
return session.get_node(node_id, WlanNode)
except CoreError:
return session.get_node(node_id, EmaneNet)
class MobilityManager(ModelManager):
"""
Member of session class for handling configuration data for mobility and
@ -69,35 +77,30 @@ class MobilityManager(ModelManager):
"""
if node_ids is None:
node_ids = self.nodes()
for node_id in node_ids:
logging.debug("checking mobility startup for node: %s", node_id)
logging.debug(
"node mobility configurations: %s", self.get_all_configs(node_id)
"node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id)
)
try:
node = self.session.get_node(node_id, WlanNode)
node = get_mobility_node(self.session, node_id)
# TODO: may be an issue if there are multiple mobility models
for model in self.models.values():
config = self.get_configs(node_id, model.name)
if not config:
continue
self.set_model(node, model, config)
if node.mobility:
self.session.event_loop.add_event(0.0, node.mobility.startup)
except CoreError:
logging.exception("mobility startup error")
logging.warning(
"skipping mobility configuration for unknown node: %s", node_id
)
continue
for model_name in self.models:
config = self.get_configs(node_id, model_name)
if not config:
continue
model_class = self.models[model_name]
self.set_model(node, model_class, config)
if node.mobility:
self.session.event_loop.add_event(0.0, node.mobility.startup)
def handleevent(self, event_data: EventData) -> None:
"""
Handle an Event Message used to start, stop, or pause
mobility scripts for a given WlanNode.
mobility scripts for a given mobility network.
:param event_data: event data to handle
:return: nothing
@ -106,40 +109,35 @@ class MobilityManager(ModelManager):
node_id = event_data.node
name = event_data.name
try:
node = self.session.get_node(node_id, WlanNode)
node = get_mobility_node(self.session, node_id)
except CoreError:
logging.exception(
"Ignoring event for model '%s', unknown node '%s'", name, node_id
"ignoring event for model(%s), unknown node(%s)", name, node_id
)
return
# name is e.g. "mobility:ns2script"
models = name[9:].split(",")
for model in models:
try:
cls = self.models[model]
except KeyError:
logging.warning("Ignoring event for unknown model '%s'", model)
cls = self.models.get(model)
if not cls:
logging.warning("ignoring event for unknown model '%s'", model)
continue
if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]:
model = node.mobility
else:
continue
if model is None:
logging.warning("Ignoring event, %s has no model", node.name)
logging.warning("ignoring event, %s has no model", node.name)
continue
if cls.name != model.name:
logging.warning(
"Ignoring event for %s wrong model %s,%s",
"ignoring event for %s wrong model %s,%s",
node.name,
cls.name,
model.name,
)
continue
if event_type in [EventTypes.STOP, EventTypes.RESTART]:
model.stop(move_initial=True)
if event_type in [EventTypes.START, EventTypes.RESTART]:
@ -162,11 +160,9 @@ class MobilityManager(ModelManager):
event_type = EventTypes.START
elif model.state == model.STATE_PAUSED:
event_type = EventTypes.PAUSE
start_time = int(model.lasttime - model.timezero)
end_time = int(model.endtime)
data = f"start={start_time} end={end_time}"
event_data = EventData(
node=model.id,
event_type=event_type,
@ -174,15 +170,14 @@ class MobilityManager(ModelManager):
data=data,
time=str(time.monotonic()),
)
self.session.broadcast_event(event_data)
def updatewlans(
def update_nets(
self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]
) -> None:
"""
A mobility script has caused nodes in the 'moved' list to move.
Update every WlanNode. This saves range calculations if the model
Update every mobility network. This saves range calculations if the model
were to recalculate for each individual node movement.
:param moved: moved nodes
@ -191,11 +186,11 @@ class MobilityManager(ModelManager):
"""
for node_id in self.nodes():
try:
node = self.session.get_node(node_id, WlanNode)
node = get_mobility_node(self.session, node_id)
if node.model:
node.model.update(moved, moved_ifaces)
except CoreError:
continue
if node.model:
node.model.update(moved, moved_ifaces)
logging.exception("error updating mobility node")
class WirelessModel(ConfigurableOptions):
@ -593,7 +588,7 @@ class WayPointMobility(WirelessModel):
self.lasttime: Optional[float] = None
self.endtime: Optional[int] = None
self.timezero: float = 0.0
self.wlan: WlanNode = session.get_node(_id, WlanNode)
self.net: Union[WlanNode, EmaneNet] = get_mobility_node(self.session, self.id)
# these are really set in child class via confmatrix
self.loop: bool = False
self.refresh_ms: int = 50
@ -601,6 +596,9 @@ class WayPointMobility(WirelessModel):
# (ns-3 sets this to False as new waypoints may be added from trace)
self.empty_queue_stop: bool = True
def startup(self):
raise NotImplementedError
def runround(self) -> None:
"""
Advance script time and move nodes.
@ -643,14 +641,14 @@ class WayPointMobility(WirelessModel):
# only move interfaces attached to self.wlan, or all nodenum in script?
moved = []
moved_ifaces = []
for iface in self.wlan.get_ifaces():
for iface in self.net.get_ifaces():
node = iface.node
if self.movenode(node, dt):
moved.append(node)
moved_ifaces.append(iface)
# calculate all ranges after moving nodes; this saves calculations
self.session.mobility.updatewlans(moved, moved_ifaces)
self.session.mobility.update_nets(moved, moved_ifaces)
# TODO: check session state
self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
@ -723,7 +721,7 @@ class WayPointMobility(WirelessModel):
"""
moved = []
moved_ifaces = []
for iface in self.wlan.get_ifaces():
for iface in self.net.get_ifaces():
node = iface.node
if node.id not in self.initial:
continue
@ -731,7 +729,7 @@ class WayPointMobility(WirelessModel):
self.setnodeposition(node, x, y, z)
moved.append(node)
moved_ifaces.append(iface)
self.session.mobility.updatewlans(moved, moved_ifaces)
self.session.mobility.update_nets(moved, moved_ifaces)
def addwaypoint(
self,
@ -1094,7 +1092,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing
"""
if self.autostart == "":
logging.info("not auto-starting ns-2 script for %s", self.wlan.name)
logging.info("not auto-starting ns-2 script for %s", self.net.name)
return
try:
t = float(self.autostart)
@ -1102,11 +1100,11 @@ class Ns2ScriptedMobility(WayPointMobility):
logging.exception(
"Invalid auto-start seconds specified '%s' for %s",
self.autostart,
self.wlan.name,
self.net.name,
)
return
self.movenodesinitial()
logging.info("scheduling ns-2 script for %s autostart at %s", self.wlan.name, t)
logging.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t)
self.state = self.STATE_RUNNING
self.session.event_loop.add_event(t, self.run)

View file

@ -10,7 +10,17 @@ services.
import enum
import logging
import time
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Type
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
Optional,
Set,
Tuple,
Type,
Union,
)
from core import utils
from core.emulator.data import FileData
@ -21,6 +31,8 @@ from core.nodes.base import CoreNode
if TYPE_CHECKING:
from core.emulator.session import Session
CoreServiceType = Union["CoreService", Type["CoreService"]]
class ServiceBootError(Exception):
pass
@ -39,95 +51,56 @@ class ServiceDependencies:
provided.
"""
def __init__(self, services: List["CoreService"]) -> None:
# helpers to check validity
self.dependents: Dict[str, Set[str]] = {}
self.booted: Set[str] = set()
self.node_services: Dict[str, "CoreService"] = {}
for service in services:
self.node_services[service.name] = service
for dependency in service.dependencies:
dependents = self.dependents.setdefault(dependency, set())
dependents.add(service.name)
# used to find paths
self.path: List["CoreService"] = []
def __init__(self, services: List["CoreServiceType"]) -> None:
self.visited: Set[str] = set()
self.visiting: Set[str] = set()
self.services: Dict[str, "CoreServiceType"] = {}
self.paths: Dict[str, List["CoreServiceType"]] = {}
self.boot_paths: List[List["CoreServiceType"]] = []
roots = set([x.name for x in services])
for service in services:
self.services[service.name] = service
roots -= set(service.dependencies)
self.roots: List["CoreServiceType"] = [x for x in services if x.name in roots]
if services and not self.roots:
raise ValueError("circular dependency is present")
def boot_paths(self) -> List[List["CoreService"]]:
"""
Generates the boot paths for the services provided to the class.
def _search(
self,
service: "CoreServiceType",
visiting: Set[str] = None,
path: List[str] = None,
) -> List["CoreServiceType"]:
if service.name in self.visited:
return self.paths[service.name]
self.visited.add(service.name)
if visiting is None:
visiting = set()
visiting.add(service.name)
if path is None:
for dependency in service.dependencies:
path = self.paths.get(dependency)
if path is not None:
break
for dependency in service.dependencies:
service_dependency = self.services.get(dependency)
if not service_dependency:
raise ValueError(f"required dependency was not provided: {dependency}")
if dependency in visiting:
raise ValueError(f"circular dependency, already visited: {dependency}")
else:
path = self._search(service_dependency, visiting, path)
visiting.remove(service.name)
if path is None:
path = []
self.boot_paths.append(path)
path.append(service)
self.paths[service.name] = path
return path
:return: list of services to boot, in order
"""
paths = []
for name in self.node_services:
service = self.node_services[name]
if service.name in self.booted:
logging.debug(
"skipping service that will already be booted: %s", service.name
)
continue
path = self._start(service)
if path:
paths.append(path)
if self.booted != set(self.node_services):
raise ValueError(
"failure to boot all services: %s != %s"
% (self.booted, self.node_services.keys())
)
return paths
def _reset(self) -> None:
self.path = []
self.visited.clear()
self.visiting.clear()
def _start(self, service: "CoreService") -> List["CoreService"]:
logging.debug("starting service dependency check: %s", service.name)
self._reset()
return self._visit(service)
def _visit(self, current_service: "CoreService") -> List["CoreService"]:
logging.debug("visiting service(%s): %s", current_service.name, self.path)
self.visited.add(current_service.name)
self.visiting.add(current_service.name)
# dive down
for service_name in current_service.dependencies:
if service_name not in self.node_services:
raise ValueError(
"required dependency was not included in node services: %s"
% service_name
)
if service_name in self.visiting:
raise ValueError(
"cyclic dependency at service(%s): %s"
% (current_service.name, service_name)
)
if service_name not in self.visited:
service = self.node_services[service_name]
self._visit(service)
# add service when bottom is found
logging.debug("adding service to boot path: %s", current_service.name)
self.booted.add(current_service.name)
self.path.append(current_service)
self.visiting.remove(current_service.name)
# rise back up
for service_name in self.dependents.get(current_service.name, []):
if service_name not in self.visited:
service = self.node_services[service_name]
self._visit(service)
return self.path
def boot_order(self) -> List[List["CoreServiceType"]]:
for service in self.roots:
self._search(service)
return self.boot_paths
class ServiceShim:
@ -470,23 +443,16 @@ class CoreServices:
:param node: node to start services on
:return: nothing
"""
boot_paths = ServiceDependencies(node.services).boot_paths()
boot_paths = ServiceDependencies(node.services).boot_order()
funcs = []
for boot_path in boot_paths:
args = (node, boot_path)
funcs.append((self._start_boot_paths, args, {}))
funcs.append((self._boot_service_path, args, {}))
result, exceptions = utils.threadpool(funcs)
if exceptions:
raise ServiceBootError(*exceptions)
def _start_boot_paths(self, node: CoreNode, boot_path: List["CoreService"]) -> None:
"""
Start all service boot paths found, based on dependencies.
:param node: node to start services on
:param boot_path: service to start in dependent order
:return: nothing
"""
def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]):
logging.info(
"booting node(%s) services: %s",
node.name,
@ -496,11 +462,11 @@ class CoreServices:
service = self.get_service(node.id, service.name, default_service=True)
try:
self.boot_service(node, service)
except Exception:
except Exception as e:
logging.exception("exception booting service: %s", service.name)
raise
raise ServiceBootError(e)
def boot_service(self, node: CoreNode, service: "CoreService") -> None:
def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None:
"""
Start a service on a node. Create private dirs, generate config
files, and execute startup commands.
@ -584,7 +550,7 @@ class CoreServices:
return True
return False
def validate_service(self, node: CoreNode, service: "CoreService") -> int:
def validate_service(self, node: CoreNode, service: "CoreServiceType") -> int:
"""
Run the validation command(s) for a service.
@ -622,7 +588,7 @@ class CoreServices:
for service in node.services:
self.stop_service(node, service)
def stop_service(self, node: CoreNode, service: "CoreService") -> int:
def stop_service(self, node: CoreNode, service: "CoreServiceType") -> int:
"""
Stop a service on a node.
@ -724,7 +690,7 @@ class CoreServices:
service.config_data[file_name] = data
def startup_service(
self, node: CoreNode, service: "CoreService", wait: bool = False
self, node: CoreNode, service: "CoreServiceType", wait: bool = False
) -> int:
"""
Startup a node service.
@ -747,7 +713,7 @@ class CoreServices:
status = -1
return status
def create_service_files(self, node: CoreNode, service: "CoreService") -> None:
def create_service_files(self, node: CoreNode, service: "CoreServiceType") -> None:
"""
Creates node service files.

211
daemon/poetry.lock generated
View file

@ -21,21 +21,20 @@ description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
version = "20.1.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "main"
description = "Modern password hashing for your software and your servers"
name = "bcrypt"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.1.7"
python-versions = ">=3.6"
version = "3.2.0"
[package.dependencies]
cffi = ">=1.1"
@ -43,6 +42,7 @@ six = ">=1.4.1"
[package.extras]
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"]
typecheck = ["mypy"]
[[package]]
category = "dev"
@ -67,7 +67,7 @@ description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = false
python-versions = "*"
version = "1.14.0"
version = "1.14.2"
[package.dependencies]
pycparser = "*"
@ -103,23 +103,24 @@ description = "cryptography is a package which provides cryptographic recipes an
name = "cryptography"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
version = "2.9.2"
version = "3.0"
[package.dependencies]
cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1"
[package.extras]
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
idna = ["idna (>=2.1)"]
pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
[[package]]
category = "main"
description = "A backport of the dataclasses module for Python 3.6"
marker = "python_version == \"3.6\""
marker = "python_version >= \"3.6\" and python_version < \"3.7\""
name = "dataclasses"
optional = false
python-versions = ">=3.6, <3.7"
@ -131,7 +132,7 @@ description = "Distribution utilities"
name = "distlib"
optional = false
python-versions = "*"
version = "0.3.0"
version = "0.3.1"
[[package]]
category = "main"
@ -203,7 +204,7 @@ description = "File identification library for Python"
name = "identify"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
version = "1.4.18"
version = "1.4.28"
[package.extras]
license = ["editdistance"]
@ -215,14 +216,14 @@ marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.6.0"
version = "1.7.0"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "importlib-resources"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
[[package]]
category = "dev"
@ -231,13 +232,9 @@ marker = "python_version < \"3.7\""
name = "importlib-resources"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.5.0"
version = "3.0.0"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = "*"
[package.dependencies.zipp]
python = "<3.8"
version = ">=0.4"
@ -331,7 +328,7 @@ description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.3.0"
version = "8.4.0"
[[package]]
category = "main"
@ -446,7 +443,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
version = "1.9.0"
[[package]]
category = "dev"
@ -560,11 +557,11 @@ description = "Virtual Python Environment builder"
name = "virtualenv"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
version = "20.0.21"
version = "20.0.31"
[package.dependencies]
appdirs = ">=1.4.3,<2"
distlib = ">=0.3.0,<1"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
six = ">=1.9.0,<2"
@ -574,11 +571,11 @@ version = ">=0.12,<2"
[package.dependencies.importlib-resources]
python = "<3.7"
version = ">=1.0,<2"
version = ">=1.0"
[package.extras]
docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"]
testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
[[package]]
category = "dev"
@ -586,7 +583,7 @@ description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.2.3"
version = "0.2.5"
[[package]]
category = "dev"
@ -602,7 +599,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "94df87a12a92ccb6512e4c30965e7ba1fe54b4fa3ff75827ca55b3de8472b30e"
content-hash = "cd09344b4f0183ada890fa9ac205e6d6410d94863e9067b5d2957274cebf374b"
python-versions = "^3.6"
[metadata.files]
@ -615,62 +612,51 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
{file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"},
{file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"},
]
bcrypt = [
{file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"},
{file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"},
{file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"},
{file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"},
{file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"},
{file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"},
{file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"},
{file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"},
{file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"},
{file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"},
{file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"},
{file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"},
{file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"},
{file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"},
{file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"},
{file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"},
{file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"},
{file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"},
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"},
{file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"},
{file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"},
{file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
]
black = [
{file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"},
{file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"},
]
cffi = [
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
{file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
{file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
{file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
{file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
{file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
{file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
{file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
{file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
{file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
{file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
{file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
{file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
{file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
{file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
{file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
{file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"},
{file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"},
{file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"},
{file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"},
{file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"},
{file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"},
{file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"},
{file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"},
{file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"},
{file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"},
{file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"},
{file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"},
{file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"},
{file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"},
{file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"},
{file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"},
]
cfgv = [
{file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"},
@ -685,32 +671,33 @@ colorama = [
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
cryptography = [
{file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"},
{file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"},
{file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"},
{file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"},
{file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"},
{file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"},
{file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"},
{file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"},
{file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"},
{file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"},
{file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"},
{file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"},
{file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"},
{file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"},
{file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"},
{file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"},
{file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"},
{file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
{file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
{file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"},
{file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"},
{file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"},
{file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"},
{file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"},
{file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"},
{file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"},
{file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"},
{file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"},
{file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"},
{file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"},
{file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"},
{file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"},
{file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"},
{file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"},
{file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"},
{file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"},
{file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"},
{file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"},
]
dataclasses = [
{file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"},
{file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"},
]
distlib = [
{file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"},
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
]
fabric = [
{file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"},
@ -815,16 +802,16 @@ grpcio-tools = [
{file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"},
]
identify = [
{file = "identify-1.4.18-py2.py3-none-any.whl", hash = "sha256:9f53e80371f2ac7c969eefda8efaabd4f77c6300f5f8fc4b634744a0db8fe5cc"},
{file = "identify-1.4.18.tar.gz", hash = "sha256:de4e1de6c23f52b71c8a54ff558219f3783ff011b432f29360d84a8a31ba561c"},
{file = "identify-1.4.28-py2.py3-none-any.whl", hash = "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6"},
{file = "identify-1.4.28.tar.gz", hash = "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"},
]
importlib-metadata = [
{file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"},
{file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"},
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
]
importlib-resources = [
{file = "importlib_resources-1.5.0-py2.py3-none-any.whl", hash = "sha256:85dc0b9b325ff78c8bef2e4ff42616094e16b98ebd5e3b50fe7e2f0bbcdcde49"},
{file = "importlib_resources-1.5.0.tar.gz", hash = "sha256:6f87df66833e1942667108628ec48900e02a4ab4ad850e25fbf07cb17cf734ca"},
{file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"},
{file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"},
]
invoke = [
{file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"},
@ -912,8 +899,8 @@ mock = [
{file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"},
]
more-itertools = [
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
]
netaddr = [
{file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"},
@ -984,8 +971,8 @@ protobuf = [
{file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"},
]
py = [
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
@ -1072,12 +1059,12 @@ toml = [
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
]
virtualenv = [
{file = "virtualenv-20.0.21-py2.py3-none-any.whl", hash = "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70"},
{file = "virtualenv-20.0.21.tar.gz", hash = "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf"},
{file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"},
{file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"},
]
wcwidth = [
{file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"},
{file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"},
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},

View file

@ -714,6 +714,19 @@ message Session {
repeated Node nodes = 3;
repeated Link links = 4;
string dir = 5;
string user = 6;
repeated services.ServiceDefaults default_services = 7;
SessionLocation location = 8;
repeated Hook hooks = 9;
repeated string emane_models = 10;
map<string, common.ConfigOption> emane_config = 11;
repeated emane.GetEmaneModelConfig emane_model_configs = 12;
map<int32, common.MappedConfig> wlan_configs = 13;
repeated services.NodeServiceConfig service_configs = 14;
repeated configservices.ConfigServiceConfig config_service_configs = 15;
map<int32, common.MappedConfig> mobility_configs = 16;
map<string, string> metadata = 17;
string file = 18;
}
message SessionSummary {

View file

@ -53,14 +53,15 @@ message GetEmaneModelConfigsRequest {
int32 session_id = 1;
}
message GetEmaneModelConfig {
int32 node_id = 1;
string model = 2;
int32 iface_id = 3;
map<string, common.ConfigOption> config = 4;
}
message GetEmaneModelConfigsResponse {
message ModelConfig {
int32 node_id = 1;
string model = 2;
int32 iface_id = 3;
map<string, common.ConfigOption> config = 4;
}
repeated ModelConfig configs = 1;
repeated GetEmaneModelConfig configs = 1;
}
message GetEmaneEventChannelRequest {

View file

@ -59,6 +59,13 @@ message NodeServiceData {
string meta = 10;
}
message NodeServiceConfig {
int32 node_id = 1;
string service = 2;
NodeServiceData data = 3;
map<string, string> files = 4;
}
message GetServicesRequest {
}
@ -89,13 +96,7 @@ message GetNodeServiceConfigsRequest {
}
message GetNodeServiceConfigsResponse {
message ServiceConfig {
int32 node_id = 1;
string service = 2;
NodeServiceData data = 3;
map<string, string> files = 4;
}
repeated ServiceConfig configs = 1;
repeated NodeServiceConfig configs = 1;
}
message GetNodeServiceRequest {

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "core"
version = "7.0.1"
version = "7.1.0"
description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause"
@ -11,25 +11,25 @@ include = ["core/gui/data/**/*", "core/configservices/*/templates"]
[tool.poetry.dependencies]
python = "^3.6"
dataclasses = { version = "*", python = "~3.6" }
fabric = "*"
fabric = "2.5.0"
grpcio = "1.27.2"
invoke = "*"
lxml = "*"
mako = "*"
netaddr = "*"
pillow = "*"
protobuf = "*"
pyproj = "*"
pyyaml = "*"
invoke = "1.4.1"
lxml = "4.5.1"
mako = "1.1.3"
netaddr = "0.7.19"
pillow = "7.1.2"
protobuf = "3.12.2"
pyproj = "2.6.1.post1"
pyyaml = "5.3.1"
[tool.poetry.dev-dependencies]
black = "==19.3b0"
flake8 = "*"
flake8 = "3.8.2"
grpcio-tools = "1.27.2"
isort = "*"
mock = "*"
pre-commit = "*"
pytest = "*"
isort = "4.3.21"
mock = "4.0.2"
pre-commit = "2.1.1"
pytest = "5.4.3"
[tool.isort]
skip_glob = "*_pb2*.py,doc,build"

View file

@ -1,3 +1,4 @@
import itertools
import os
import pytest
@ -15,40 +16,6 @@ SERVICE_ONE = "MyService"
SERVICE_TWO = "MyService2"
class ServiceA(CoreService):
name = "A"
dependencies = ("B",)
class ServiceB(CoreService):
name = "B"
dependencies = ()
class ServiceC(CoreService):
name = "C"
dependencies = ("B", "D")
class ServiceD(CoreService):
name = "D"
dependencies = ()
class ServiceBadDependency(CoreService):
name = "E"
dependencies = ("Z",)
class ServiceF(CoreService):
name = "F"
dependencies = ()
class ServiceCycleDependency(CoreService):
name = "G"
class TestServices:
def test_service_all_files(self, session: Session):
# given
@ -253,37 +220,157 @@ class TestServices:
assert default_service == my_service
assert custom_service and custom_service != my_service
def test_services_dependencies(self):
def test_services_dependency(self):
# given
services = [ServiceA, ServiceB, ServiceC, ServiceD, ServiceF]
service_a = CoreService()
service_a.name = "a"
service_b = CoreService()
service_b.name = "b"
service_c = CoreService()
service_c.name = "c"
service_d = CoreService()
service_d.name = "d"
service_e = CoreService()
service_e.name = "e"
service_a.dependencies = (service_b.name,)
service_b.dependencies = ()
service_c.dependencies = (service_b.name, service_d.name)
service_d.dependencies = ()
service_e.dependencies = ()
services = [service_a, service_b, service_c, service_d, service_e]
expected1 = {service_a.name, service_b.name, service_c.name, service_d.name}
expected2 = [service_e]
# when
boot_paths = ServiceDependencies(services).boot_paths()
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
results = ServiceDependencies(permutation).boot_order()
# then
for result in results:
result_set = {x.name for x in result}
if len(result) == 4:
a_index = result.index(service_a)
b_index = result.index(service_b)
c_index = result.index(service_c)
d_index = result.index(service_d)
assert b_index < a_index
assert b_index < c_index
assert d_index < c_index
assert result_set == expected1
elif len(result) == 1:
assert expected2 == result
else:
raise ValueError(
f"unexpected result: {results}, perm({permutation})"
)
# then
assert len(boot_paths) == 2
def test_services_dependencies_not_present(self):
def test_services_dependency_missing(self):
# given
services = [
ServiceA,
ServiceB,
ServiceC,
ServiceD,
ServiceF,
ServiceBadDependency,
]
service_a = CoreService()
service_a.name = "a"
service_b = CoreService()
service_b.name = "b"
service_c = CoreService()
service_c.name = "c"
service_a.dependencies = (service_b.name,)
service_b.dependencies = (service_c.name,)
service_c.dependencies = ("d",)
services = [service_a, service_b, service_c]
# when, then
with pytest.raises(ValueError):
ServiceDependencies(services).boot_paths()
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
with pytest.raises(ValueError):
ServiceDependencies(permutation).boot_order()
def test_services_dependencies_cycle(self):
def test_services_dependency_cycle(self):
# given
service_d = ServiceD()
service_d.dependencies = ("C",)
services = [ServiceA, ServiceB, ServiceC, service_d, ServiceF]
service_a = CoreService()
service_a.name = "a"
service_b = CoreService()
service_b.name = "b"
service_c = CoreService()
service_c.name = "c"
service_a.dependencies = (service_b.name,)
service_b.dependencies = (service_c.name,)
service_c.dependencies = (service_a.name,)
services = [service_a, service_b, service_c]
# when, then
with pytest.raises(ValueError):
ServiceDependencies(services).boot_paths()
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
with pytest.raises(ValueError):
ServiceDependencies(permutation).boot_order()
def test_services_dependency_common(self):
# given
service_a = CoreService()
service_a.name = "a"
service_b = CoreService()
service_b.name = "b"
service_c = CoreService()
service_c.name = "c"
service_d = CoreService()
service_d.name = "d"
service_a.dependencies = (service_b.name,)
service_c.dependencies = (service_d.name, service_b.name)
services = [service_a, service_b, service_c, service_d]
expected = {service_a.name, service_b.name, service_c.name, service_d.name}
# when
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
results = ServiceDependencies(permutation).boot_order()
# then
for result in results:
assert len(result) == 4
result_set = {x.name for x in result}
a_index = result.index(service_a)
b_index = result.index(service_b)
c_index = result.index(service_c)
d_index = result.index(service_d)
assert b_index < a_index
assert d_index < c_index
assert b_index < c_index
assert expected == result_set
def test_services_dependency_common2(self):
# given
service_a = CoreService()
service_a.name = "a"
service_b = CoreService()
service_b.name = "b"
service_c = CoreService()
service_c.name = "c"
service_d = CoreService()
service_d.name = "d"
service_a.dependencies = (service_b.name,)
service_b.dependencies = (service_c.name, service_d.name)
service_c.dependencies = (service_d.name,)
services = [service_a, service_b, service_c, service_d]
expected = {service_a.name, service_b.name, service_c.name, service_d.name}
# when
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
results = ServiceDependencies(permutation).boot_order()
# then
for result in results:
assert len(result) == 4
result_set = {x.name for x in result}
a_index = result.index(service_a)
b_index = result.index(service_b)
c_index = result.index(service_c)
d_index = result.index(service_d)
assert b_index < a_index
assert c_index < b_index
assert d_index < b_index
assert d_index < c_index
assert expected == result_set

View file

@ -50,23 +50,6 @@ can also subscribe to EMANE location events and move the nodes on the canvas
as they are moved in the EMANE emulation. This would occur when an Emulation
Script Generator, for example, is running a mobility script.
## EMANE Installation
EMANE can be installed from deb or RPM packages or from source. See the
[EMANE GitHub](https://github.com/adjacentlink/emane) for full details.
Here are quick instructions for installing all EMANE packages for Ubuntu 18.04:
```shell
# install dependencies
sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl
wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
# install base emane packages
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb
# install python3 bindings
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb
```
## EMANE Configuration
The CORE configuration file **/etc/core/core.conf** has options specific to

View file

@ -132,30 +132,14 @@ After the installation complete it will have installed the following scripts.
If you create your own python scripts to run CORE directly or using the gRPC/TLV
APIs you will need to make sure you are running them within context of the
installed virtual environment.
installed virtual environment. To help support this CORE provides the `core-python`
executable. This executable will allow you to enter CORE's python virtual
environment interpreter or to run a script within it.
> **NOTE:** the following assumes CORE has been installed successfully
There is an invoke task to help with this case.
```shell
cd <CORE_REPO>
inv -h run
Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...]
Docstring:
runs a user script in the core virtual environment
Options:
-f STRING, --file=STRING script file to run in the core virtual environment
-s, --sudo run script as sudo
```
Another way would be to enable the core virtual environment shell. Which
would allow you to run scripts in a more **normal** way.
```shell
cd <CORE_REPO>/daemon
poetry shell
python run /path/to/script.py
core-python <script>
```
## Manually Install EMANE
@ -199,7 +183,6 @@ Available tasks:
install-emane install emane and the python bindings
install-scripts install core script files, modified to leverage virtual environment
install-service install systemd core service
run runs a user script in the core virtual environment
test run core tests
test-emane run core emane tests
test-mock run core tests using mock to avoid running as sudo

View file

@ -271,6 +271,18 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX):
else:
c.run(f"sudo cp {script} {dest}", hide=hide)
# setup core python helper
core_python = bin_dir.joinpath("core-python")
temp = NamedTemporaryFile("w", delete=False)
temp.writelines([
"#!/bin/bash\n",
f'exec "{python}" "$@"\n',
])
temp.close()
c.run(f"sudo cp {temp.name} {core_python}", hide=hide)
c.run(f"sudo chmod 755 {core_python}", hide=hide)
os.unlink(temp.name)
# install core configuration file
config_dir = "/etc/core"
c.run(f"sudo mkdir -p {config_dir}", hide=hide)
@ -312,7 +324,7 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX):
install_service(c, hide, prefix)
with p.start("installing ospf mdr"):
install_ospf_mdr(c, os_info, hide)
print("\nyou may need to open a new terminal to leverage invoke for running core")
print("\ninstall complete!")
@task(
@ -402,6 +414,10 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX):
dest = bin_dir.joinpath(script.name)
c.run(f"sudo rm -f {dest}", hide=hide)
# remove core-python symlink
core_python = bin_dir.joinpath("core-python")
c.run(f"sudo rm -f {core_python}", hide=hide)
# install service
systemd_dir = Path("/lib/systemd/system/")
service_name = "core-daemon.service"
@ -453,28 +469,6 @@ def daemon(c):
)
@task(
help={
"sudo": "run script as sudo",
"file": "script file to run in the core virtual environment"
},
)
def run(c, file, sudo=False):
"""
runs a user script in the core virtual environment
"""
if not file:
print("no script was provided")
return
python = get_python(c)
path = Path(file).absolute()
with c.cd(DAEMON_DIR):
cmd = f"{python} {path}"
if sudo:
cmd = f"sudo {cmd}"
c.run(cmd, pty=True)
@task
def test(c):
"""