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 ## 2020-07-28 CORE 7.0.1
* Bugfixes * Bugfixes

View file

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

View file

@ -552,11 +552,12 @@ class CoreGrpcClient:
source: str = None, source: str = None,
) -> core_pb2.EditNodeResponse: ) -> 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 session_id: session id
:param node_id: node 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 icon: path to icon for gui to use for node
:param geo: lon,lat,alt location for node :param geo: lon,lat,alt location for node
:param source: application source :param source: application source

View file

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

View file

@ -8,14 +8,25 @@ from grpc import ServicerContext
from core import utils from core import utils
from core.api.grpc import common_pb2, core_pb2 from core.api.grpc import common_pb2, core_pb2
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig from core.api.grpc.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.config import ConfigurableOptions
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.session import Session from core.emulator.session import Session
from core.nodes.base import 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.interface import CoreInterface
from core.nodes.network import WlanNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
WORKERS = 10 WORKERS = 10
@ -218,7 +229,7 @@ def get_config_options(
""" """
results = {} results = {}
for configuration in configurable_options.configurations(): for configuration in configurable_options.configurations():
value = config[configuration.id] value = config.get(configuration.id, configuration.default)
config_option = common_pb2.ConfigOption( config_option = common_pb2.ConfigOption(
label=configuration.label, label=configuration.label,
name=configuration.id, name=configuration.id,
@ -536,3 +547,131 @@ def get_nem_id(
message = f"{node.name} interface {iface_id} nem id does not exist" message = f"{node.name} interface {iface_id} nem id does not exist"
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message) context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
return nem_id 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, core_pb2_grpc,
grpcutils, grpcutils,
) )
from core.api.grpc.common_pb2 import MappedConfig
from core.api.grpc.configservices_pb2 import ( from core.api.grpc.configservices_pb2 import (
ConfigService, ConfigService,
GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsRequest,
@ -89,7 +88,6 @@ from core.api.grpc.services_pb2 import (
ServiceAction, ServiceAction,
ServiceActionRequest, ServiceActionRequest,
ServiceActionResponse, ServiceActionResponse,
ServiceDefaults,
SetNodeServiceFileRequest, SetNodeServiceFileRequest,
SetNodeServiceFileResponse, SetNodeServiceFileResponse,
SetNodeServiceRequest, SetNodeServiceRequest,
@ -118,12 +116,13 @@ from core.emulator.enumerations import (
from core.emulator.session import NT, Session from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.network import PtpNet, WlanNode from core.nodes.network import PtpNet, WlanNode
from core.services.coreservices import ServiceManager from core.services.coreservices import ServiceManager
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)") _INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
_MAX_WORKERS = 1000
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
@ -152,7 +151,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
def listen(self, address: str) -> None: def listen(self, address: str) -> None:
logging.info("CORE gRPC API listening on: %s", address) 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) core_pb2_grpc.add_CoreApiServicer_to_server(self, self.server)
self.server.add_insecure_port(address) self.server.add_insecure_port(address)
self.server.start() self.server.start()
@ -558,7 +557,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get session: %s", request) logging.debug("get session: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
links = [] links = []
nodes = [] nodes = []
for _id in session.nodes: for _id in session.nodes:
@ -568,9 +566,39 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nodes.append(node_proto) nodes.append(node_proto)
node_links = get_links(node) node_links = get_links(node)
links.extend(node_links) 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( 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) return core_pb2.GetSessionResponse(session=session_proto)
@ -652,9 +680,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
key = key.split(".") key = key.split(".")
node_id = _INTERFACE_REGEX.search(key[0]).group("node") node_id = _INTERFACE_REGEX.search(key[0]).group("node")
node_id = int(node_id, base=16) node_id = int(node_id, base=16)
iface_id = int(key[1], base=16) iface_id = int(key[1])
session_id = int(key[2], base=16) session_id = key[2]
if session.id != session_id: if session.short_session_id() != session_id:
continue continue
iface_throughput = throughputs_event.iface_throughputs.add() iface_throughput = throughputs_event.iface_throughputs.add()
iface_throughput.node_id = node_id iface_throughput.node_id = node_id
@ -664,8 +692,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
try: try:
key = key.split(".") key = key.split(".")
node_id = int(key[1], base=16) node_id = int(key[1], base=16)
session_id = int(key[2], base=16) session_id = key[2]
if session.id != session_id: if session.short_session_id() != session_id:
continue continue
bridge_throughput = ( bridge_throughput = (
throughputs_event.bridge_throughputs.add() throughputs_event.bridge_throughputs.add()
@ -1012,12 +1040,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get hooks: %s", request) logging.debug("get hooks: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
hooks = [] hooks = grpcutils.get_hooks(session)
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 core_pb2.GetHooksResponse(hooks=hooks) return core_pb2.GetHooksResponse(hooks=hooks)
def AddHook( def AddHook(
@ -1050,19 +1073,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get mobility configs: %s", request) logging.debug("get mobility configs: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
response = GetMobilityConfigsResponse() configs = grpcutils.get_mobility_configs(session)
for node_id in session.mobility.node_configurations: return GetMobilityConfigsResponse(configs=configs)
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
def GetMobilityConfig( def GetMobilityConfig(
self, request: GetMobilityConfigRequest, context: ServicerContext self, request: GetMobilityConfigRequest, context: ServicerContext
@ -1115,7 +1127,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("mobility action: %s", request) logging.debug("mobility action: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
node = self.get_node(session, request.node_id, context, 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 result = True
if request.action == MobilityAction.START: if request.action == MobilityAction.START:
node.mobility.start() node.mobility.start()
@ -1157,12 +1173,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get service defaults: %s", request) logging.debug("get service defaults: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
all_service_defaults = [] defaults = grpcutils.get_default_services(session)
for node_type in session.services.default_services: return GetServiceDefaultsResponse(defaults=defaults)
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)
def SetServiceDefaults( def SetServiceDefaults(
self, request: SetServiceDefaultsRequest, context: ServicerContext self, request: SetServiceDefaultsRequest, context: ServicerContext
@ -1196,18 +1208,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get node service configs: %s", request) logging.debug("get node service configs: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
configs = [] configs = grpcutils.get_node_service_configs(session)
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)
return GetNodeServiceConfigsResponse(configs=configs) return GetNodeServiceConfigsResponse(configs=configs)
def GetNodeService( def GetNodeService(
@ -1337,19 +1338,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get wlan configs: %s", request) logging.debug("get wlan configs: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
response = GetWlanConfigsResponse() configs = grpcutils.get_wlan_configs(session)
for node_id in session.mobility.node_configurations: return GetWlanConfigsResponse(configs=configs)
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
def GetWlanConfig( def GetWlanConfig(
self, request: GetWlanConfigRequest, context: ServicerContext self, request: GetWlanConfigRequest, context: ServicerContext
@ -1401,8 +1391,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get emane config: %s", request) logging.debug("get emane config: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
current_config = session.emane.get_configs() config = grpcutils.get_emane_config(session)
config = get_config_options(current_config, session.emane.emane_config)
return GetEmaneConfigResponse(config=config) return GetEmaneConfigResponse(config=config)
def SetEmaneConfig( def SetEmaneConfig(
@ -1433,11 +1422,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get emane models: %s", request) logging.debug("get emane models: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
models = [] models = grpcutils.get_emane_models(session)
for model in session.emane.models.keys():
if len(model.split("_")) != 2:
continue
models.append(model)
return GetEmaneModelsResponse(models=models) return GetEmaneModelsResponse(models=models)
def GetEmaneModelConfig( def GetEmaneModelConfig(
@ -1491,22 +1476,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logging.debug("get emane model configs: %s", request) logging.debug("get emane model configs: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
configs = grpcutils.get_emane_model_configs(session)
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)
return GetEmaneModelConfigsResponse(configs=configs) return GetEmaneModelConfigsResponse(configs=configs)
def SaveXml( def SaveXml(
@ -1713,21 +1683,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
:return: get node config service configs response :return: get node config service configs response
""" """
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
configs = [] configs = grpcutils.get_node_config_service_configs(session)
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)
return GetNodeConfigServiceConfigsResponse(configs=configs) return GetNodeConfigServiceConfigsResponse(configs=configs)
def GetNodeConfigServices( def GetNodeConfigServices(

View file

@ -520,8 +520,8 @@ class CoreLinkTlv(CoreTlv):
LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr, LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr, LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16, LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString, LinkTlvs.IFACE1_NAME.value: CoreTlvDataString,
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString, LinkTlvs.IFACE2_NAME.value: CoreTlvDataString,
LinkTlvs.OPAQUE.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) node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
iface1_data = InterfaceData( iface1_data = InterfaceData(
id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value), 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), mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
@ -768,7 +768,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
) )
iface2_data = InterfaceData( iface2_data = InterfaceData(
id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value), 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), mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value), ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value), ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
@ -1903,12 +1903,12 @@ class CoreHandler(socketserver.BaseRequestHandler):
# TODO: send location info # TODO: send location info
# send hook scripts # send hook scripts
for state in sorted(self.session.hooks.keys()): for state in sorted(self.session.hooks):
for file_name, config_data in self.session.hooks[state]: for file_name, config_data in self.session.hooks[state]:
file_data = FileData( file_data = FileData(
message_type=MessageFlags.ADD, message_type=MessageFlags.ADD,
name=str(file_name), name=str(file_name),
type=f"hook:{state}", type=f"hook:{state.value}",
data=str(config_data), data=str(config_data),
) )
self.session.broadcast_file(file_data) self.session.broadcast_file(file_data)

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import os
import pwd import pwd
import shutil import shutil
import subprocess import subprocess
import sys
import tempfile import tempfile
import threading import threading
import time import time
@ -119,7 +120,7 @@ class Session:
# states and hooks handlers # states and hooks handlers
self.state: EventTypes = EventTypes.DEFINITION_STATE self.state: EventTypes = EventTypes.DEFINITION_STATE
self.state_time: float = time.monotonic() 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.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {}
self.add_state_hook( self.add_state_hook(
state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook
@ -621,7 +622,8 @@ class Session:
def is_active(self) -> bool: 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 :return: True if active, False otherwise
""" """
@ -991,6 +993,7 @@ class Session:
:return: environment variables :return: environment variables
""" """
env = os.environ.copy() env = os.environ.copy()
env["CORE_PYTHON"] = sys.executable
env["SESSION"] = str(self.id) env["SESSION"] = str(self.id)
env["SESSION_SHORT"] = self.short_session_id() env["SESSION_SHORT"] = self.short_session_id()
env["SESSION_DIR"] = self.session_dir env["SESSION_DIR"] = self.session_dir
@ -1098,7 +1101,8 @@ class Session:
def delete_node(self, _id: int) -> bool: 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 :param _id: id of node to delete
:return: True if node deleted, False otherwise :return: True if node deleted, False otherwise

View file

@ -111,13 +111,13 @@ class Application(ttk.Frame):
self.master.columnconfigure(0, weight=1) self.master.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.columnconfigure(1, weight=1) self.columnconfigure(1, weight=1)
self.grid(sticky="nsew") self.grid(sticky=tk.NSEW)
self.toolbar = Toolbar(self) self.toolbar = Toolbar(self)
self.toolbar.grid(sticky="ns") self.toolbar.grid(sticky=tk.NS)
self.right_frame = ttk.Frame(self) self.right_frame = ttk.Frame(self)
self.right_frame.columnconfigure(0, weight=1) self.right_frame.columnconfigure(0, weight=1)
self.right_frame.rowconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1)
self.right_frame.grid(row=0, column=1, sticky="nsew") self.right_frame.grid(row=0, column=1, sticky=tk.NSEW)
self.draw_canvas() self.draw_canvas()
self.draw_infobar() self.draw_infobar()
self.draw_status() self.draw_status()
@ -139,21 +139,21 @@ class Application(ttk.Frame):
canvas_frame = ttk.Frame(self.right_frame) canvas_frame = ttk.Frame(self.right_frame)
canvas_frame.rowconfigure(0, weight=1) canvas_frame.rowconfigure(0, weight=1)
canvas_frame.columnconfigure(0, weight=1) canvas_frame.columnconfigure(0, weight=1)
canvas_frame.grid(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 = 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 = 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( scroll_x = ttk.Scrollbar(
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview 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(xscrollcommand=scroll_x.set)
self.canvas.configure(yscrollcommand=scroll_y.set) self.canvas.configure(yscrollcommand=scroll_y.set)
def draw_status(self) -> None: def draw_status(self) -> None:
self.statusbar = StatusBar(self.right_frame, self) self.statusbar = StatusBar(self.right_frame, self)
self.statusbar.grid(sticky="ew", columnspan=2) self.statusbar.grid(sticky=tk.EW, columnspan=2)
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None: def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
if not self.show_infobar.get(): if not self.show_infobar.get():
@ -161,7 +161,7 @@ class Application(ttk.Frame):
self.clear_info() self.clear_info()
self.info_frame = frame_class(self.infobar, **kwargs) self.info_frame = frame_class(self.infobar, **kwargs)
self.info_frame.draw() self.info_frame.draw()
self.info_frame.grid(sticky="nsew") self.info_frame.grid(sticky=tk.NSEW)
def clear_info(self) -> None: def clear_info(self) -> None:
if self.info_frame: if self.info_frame:
@ -174,7 +174,7 @@ class Application(ttk.Frame):
def show_info(self) -> None: def show_info(self) -> None:
self.default_info() 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: def hide_info(self) -> None:
self.infobar.grid_forget() 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 = CodeText(self.top)
codetext.text.insert("1.0", LICENSE) codetext.text.insert("1.0", LICENSE)
codetext.text.config(state=tk.DISABLED) codetext.text.config(state=tk.DISABLED)
codetext.grid(sticky="nsew") codetext.grid(sticky=tk.NSEW)
label = ttk.Label( label = ttk.Label(
self.top, text="Icons from https://icons8.com", anchor=tk.CENTER 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 tkinter import ttk
from typing import TYPE_CHECKING, Dict, Optional 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.dialogs.dialog import Dialog
from core.gui.themes import PADX, PADY from core.gui.themes import PADX, PADY
from core.gui.widgets import CodeText from core.gui.widgets import CodeText
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -30,13 +30,13 @@ class AlertsDialog(Dialog):
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1) frame.rowconfigure(0, weight=1)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky=tk.NSEW, pady=PADY)
self.tree = ttk.Treeview( self.tree = ttk.Treeview(
frame, frame,
columns=("time", "level", "session_id", "node", "source"), columns=("time", "level", "session_id", "node", "source"),
show="headings", 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.column("time", stretch=tk.YES)
self.tree.heading("time", text="Time") self.tree.heading("time", text="Time")
self.tree.column("level", stretch=tk.YES, width=100) self.tree.column("level", stretch=tk.YES, width=100)
@ -49,9 +49,8 @@ class AlertsDialog(Dialog):
self.tree.heading("source", text="Source") self.tree.heading("source", text="Source")
self.tree.bind("<<TreeviewSelect>>", self.click_select) self.tree.bind("<<TreeviewSelect>>", self.click_select)
for alarm in self.app.statusbar.core_alarms: for exception in self.app.statusbar.core_alarms:
exception = alarm.exception_event level_name = exception.level.name
level_name = ExceptionLevel.Enum.Name(exception.level)
node_id = exception.node_id if exception.node_id else "" node_id = exception.node_id if exception.node_id else ""
insert_id = self.tree.insert( insert_id = self.tree.insert(
"", "",
@ -60,43 +59,43 @@ class AlertsDialog(Dialog):
values=( values=(
exception.date, exception.date,
level_name, level_name,
alarm.session_id, exception.session_id,
node_id, node_id,
exception.source, exception.source,
), ),
tags=(level_name,), 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") 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") 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") 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") self.tree.tag_configure(notice_name, background="#85e085")
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) 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) self.tree.configure(yscrollcommand=yscrollbar.set)
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) 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.tree.configure(xscrollcommand=xscrollbar.set)
self.codetext = CodeText(self.top) self.codetext = CodeText(self.top)
self.codetext.text.config(state=tk.DISABLED, height=11) 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 = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Reset", command=self.reset_alerts) button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
button.grid(row=0, column=0, sticky="ew", padx=PADX) button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
button = ttk.Button(frame, text="Close", command=self.destroy) button = ttk.Button(frame, text="Close", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky=tk.EW)
def reset_alerts(self) -> None: def reset_alerts(self) -> None:
self.codetext.text.config(state=tk.NORMAL) self.codetext.text.config(state=tk.NORMAL)
@ -108,8 +107,8 @@ class AlertsDialog(Dialog):
def click_select(self, event: tk.Event) -> None: def click_select(self, event: tk.Event) -> None:
current = self.tree.selection()[0] 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.config(state=tk.NORMAL)
self.codetext.text.delete(1.0, tk.END) 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) self.codetext.text.config(state=tk.DISABLED)

View file

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

View file

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

View file

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

View file

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

View file

@ -38,41 +38,40 @@ class CopyServiceConfigDialog(Dialog):
label = ttk.Label( label = ttk.Label(
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER 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 = 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 = listbox_scroll.listbox
for canvas_node in self.app.canvas.nodes.values(): for node in self.app.core.session.nodes.values():
file_configs = canvas_node.service_file_configs.get(self.service) file_configs = node.service_file_configs.get(self.service)
if not file_configs: if not file_configs:
continue continue
data = file_configs.get(self.file_name) data = file_configs.get(self.file_name)
if not data: if not data:
continue continue
name = canvas_node.core_node.name self.nodes[node.name] = node.id
self.nodes[name] = canvas_node.id self.listbox.insert(tk.END, node.name)
self.listbox.insert(tk.END, name)
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Copy", command=self.click_copy) button = ttk.Button(frame, text="Copy", command=self.click_copy)
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 = 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 = 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: def click_copy(self) -> None:
selection = self.listbox.curselection() selection = self.listbox.curselection()
if not selection: if not selection:
return return
name = self.listbox.get(selection) name = self.listbox.get(selection)
canvas_node_id = self.nodes[name] node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id] node = self.app.core.session.nodes[node_id]
data = canvas_node.service_file_configs[self.service][self.file_name] data = node.service_file_configs[self.service][self.file_name]
self.dialog.temp_service_files[self.file_name] = data self.dialog.temp_service_files[self.file_name] = data
self.dialog.modified_files.add(self.file_name) self.dialog.modified_files.add(self.file_name)
self.dialog.service_file_data.text.delete(1.0, tk.END) self.dialog.service_file_data.text.delete(1.0, tk.END)
@ -84,9 +83,9 @@ class CopyServiceConfigDialog(Dialog):
if not selection: if not selection:
return return
name = self.listbox.get(selection) name = self.listbox.get(selection)
canvas_node_id = self.nodes[name] node_id = self.nodes[name]
canvas_node = self.app.canvas.nodes[canvas_node_id] node = self.app.core.session.nodes[node_id]
data = canvas_node.service_file_configs[self.service][self.file_name] data = node.service_file_configs[self.service][self.file_name]
dialog = ViewConfigDialog( dialog = ViewConfigDialog(
self.app, self, name, self.service, self.file_name, data 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.columnconfigure(0, weight=1)
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
self.service_data = CodeText(self.top) 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.insert(tk.END, self.data)
self.service_data.text.config(state=tk.DISABLED) self.service_data.text.config(state=tk.DISABLED)
button = ttk.Button(self.top, text="Close", command=self.destroy) button = ttk.Button(self.top, text="Close", command=self.destroy)
button.grid(sticky="ew") button.grid(sticky=tk.EW)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, Optional
import netaddr import netaddr
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import Node
from core.gui import nodeutils, validation from core.gui import nodeutils, validation
from core.gui.appconfig import ICONS_PATH from core.gui.appconfig import ICONS_PATH
from core.gui.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
@ -16,6 +15,7 @@ from core.gui.images import Images
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import ListboxScroll, image_chooser from core.gui.widgets import ListboxScroll, image_chooser
from core.gui.wrappers import Node
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -126,12 +126,12 @@ class NodeConfigDialog(Dialog):
# field frame # field frame
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
# icon field # icon field
label = ttk.Label(frame, text="Icon") 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( self.image_button = ttk.Button(
frame, frame,
text="Icon", text="Icon",
@ -139,49 +139,49 @@ class NodeConfigDialog(Dialog):
compound=tk.NONE, compound=tk.NONE,
command=self.click_icon, 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 row += 1
# name field # name field
label = ttk.Label(frame, text="Name") label = ttk.Label(frame, text="Name")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state) 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 row += 1
# node type field # node type field
if NodeUtils.is_model_node(self.node.type): if NodeUtils.is_model_node(self.node.type):
label = ttk.Label(frame, text="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( combobox = ttk.Combobox(
frame, frame,
textvariable=self.type, textvariable=self.type,
values=list(NodeUtils.NODE_MODELS), values=list(NodeUtils.NODE_MODELS),
state=combo_state, state=combo_state,
) )
combobox.grid(row=row, column=1, sticky="ew") combobox.grid(row=row, column=1, sticky=tk.EW)
row += 1 row += 1
# container image field # container image field
if NodeUtils.is_image_node(self.node.type): if NodeUtils.is_image_node(self.node.type):
label = ttk.Label(frame, text="Image") label = ttk.Label(frame, text="Image")
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
entry = ttk.Entry(frame, textvariable=self.container_image, state=state) 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 row += 1
if NodeUtils.is_container_node(self.node.type): if NodeUtils.is_container_node(self.node.type):
# server # server
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Server") 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 = ["localhost"]
servers.extend(list(sorted(self.app.core.servers.keys()))) servers.extend(list(sorted(self.app.core.servers.keys())))
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, textvariable=self.server, values=servers, state=combo_state 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 row += 1
if NodeUtils.is_rj45_node(self.node.type): if NodeUtils.is_rj45_node(self.node.type):
@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog):
ifaces = ListboxScroll(frame) ifaces = ListboxScroll(frame)
ifaces.listbox.config(state=state) ifaces.listbox.config(state=state)
ifaces.grid( 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[:]): for inf in sorted(response.ifaces[:]):
ifaces.listbox.insert(tk.END, inf) ifaces.listbox.insert(tk.END, inf)
@ -206,13 +206,13 @@ class NodeConfigDialog(Dialog):
def draw_ifaces(self) -> None: def draw_ifaces(self) -> None:
notebook = ttk.Notebook(self.top) 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) self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
for iface_id in sorted(self.canvas_node.ifaces): for iface_id in sorted(self.canvas_node.ifaces):
iface = self.canvas_node.ifaces[iface_id] iface = self.canvas_node.ifaces[iface_id]
tab = ttk.Frame(notebook, padding=FRAME_PAD) tab = ttk.Frame(notebook, padding=FRAME_PAD)
tab.grid(sticky="nsew", pady=PADY) tab.grid(sticky=tk.NSEW, pady=PADY)
tab.columnconfigure(1, weight=1) tab.columnconfigure(1, weight=1)
tab.columnconfigure(2, weight=1) tab.columnconfigure(2, weight=1)
notebook.add(tab, text=iface.name) notebook.add(tab, text=iface.name)
@ -226,14 +226,16 @@ class NodeConfigDialog(Dialog):
text=f"Configure EMANE {emane_model}", text=f"Configure EMANE {emane_model}",
command=lambda: self.click_emane_config(emane_model, iface.id), command=lambda: self.click_emane_config(emane_model, iface.id),
) )
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY)
row += 1 row += 1
label = ttk.Label(tab, text="MAC") label = ttk.Label(tab, text="MAC")
label.grid(row=row, column=0, padx=PADX, pady=PADY) label.grid(row=row, column=0, padx=PADX, pady=PADY)
auto_set = not iface.mac auto_set = not iface.mac
mac_state = tk.DISABLED if auto_set else tk.NORMAL
is_auto = tk.BooleanVar(value=auto_set) 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( checkbutton = ttk.Checkbutton(
tab, text="Auto?", variable=is_auto, state=state tab, text="Auto?", variable=is_auto, state=state
) )
@ -241,7 +243,7 @@ class NodeConfigDialog(Dialog):
checkbutton.grid(row=row, column=1, padx=PADX) checkbutton.grid(row=row, column=1, padx=PADX)
mac = tk.StringVar(value=iface.mac) mac = tk.StringVar(value=iface.mac)
entry = ttk.Entry(tab, textvariable=mac, state=mac_state) entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
entry.grid(row=row, column=2, sticky="ew") entry.grid(row=row, column=2, sticky=tk.EW)
func = partial(mac_auto, is_auto, entry, mac) func = partial(mac_auto, is_auto, entry, mac)
checkbutton.config(command=func) checkbutton.config(command=func)
row += 1 row += 1
@ -253,7 +255,7 @@ class NodeConfigDialog(Dialog):
ip4_net = f"{iface.ip4}/{iface.ip4_mask}" ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
ip4 = tk.StringVar(value=ip4_net) ip4 = tk.StringVar(value=ip4_net)
entry = ttk.Entry(tab, textvariable=ip4, state=state) entry = ttk.Entry(tab, textvariable=ip4, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew") entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
row += 1 row += 1
label = ttk.Label(tab, text="IPv6") label = ttk.Label(tab, text="IPv6")
@ -263,21 +265,21 @@ class NodeConfigDialog(Dialog):
ip6_net = f"{iface.ip6}/{iface.ip6_mask}" ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
ip6 = tk.StringVar(value=ip6_net) ip6 = tk.StringVar(value=ip6_net)
entry = ttk.Entry(tab, textvariable=ip6, state=state) entry = ttk.Entry(tab, textvariable=ip6, state=state)
entry.grid(row=row, column=1, columnspan=2, sticky="ew") entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
def draw_buttons(self) -> None: def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
button = ttk.Button(frame, text="Apply", command=self.click_apply) button = ttk.Button(frame, text="Apply", command=self.click_apply)
button.grid(row=0, column=0, padx=PADX, sticky="ew") button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
button = ttk.Button(frame, text="Cancel", command=self.destroy) button = ttk.Button(frame, text="Cancel", command=self.destroy)
button.grid(row=0, column=1, sticky="ew") button.grid(row=0, column=1, sticky=tk.EW)
def click_emane_config(self, emane_model: str, iface_id: int) -> None: def click_emane_config(self, emane_model: str, iface_id: int) -> None:
dialog = EmaneModelDialog( 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.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll from core.gui.widgets import CheckboxList, ListboxScroll
from core.gui.wrappers import Node
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeConfigServiceDialog(Dialog): class NodeConfigServiceDialog(Dialog):
def __init__( def __init__(
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None self, app: "Application", node: Node, services: Set[str] = None
) -> None: ) -> None:
title = f"{canvas_node.core_node.name} Config Services" title = f"{node.name} Config Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node: "CanvasNode" = canvas_node self.node: Node = node
self.node_id: int = canvas_node.core_node.id
self.groups: Optional[ListboxScroll] = None self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
if services is 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.current_services: Set[str] = services
self.draw() self.draw()
@ -42,32 +41,32 @@ class NodeConfigServiceDialog(Dialog):
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) 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.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame) 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): for group in sorted(self.app.core.config_services_groups):
self.groups.listbox.insert(tk.END, group) self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change) self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0) self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services") 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.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList( self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD 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 = 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.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame) self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew") self.current.grid(sticky=tk.NSEW)
self.draw_current_services() self.draw_current_services()
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
@ -75,13 +74,13 @@ class NodeConfigServiceDialog(Dialog):
for i in range(4): for i in range(4):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Configure", command=self.click_configure) 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 = 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 = 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 = 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 # trigger group change
self.handle_group_change() self.handle_group_change()
@ -102,7 +101,7 @@ class NodeConfigServiceDialog(Dialog):
elif not var.get() and name in self.current_services: elif not var.get() and name in self.current_services:
self.current_services.remove(name) self.current_services.remove(name)
self.draw_current_services() 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: def click_configure(self) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
@ -111,8 +110,7 @@ class NodeConfigServiceDialog(Dialog):
self, self,
self.app, self.app,
self.current.listbox.get(current_selection[0]), self.current.listbox.get(current_selection[0]),
self.canvas_node, self.node,
self.node_id,
) )
if not dialog.has_error: if not dialog.has_error:
dialog.show() dialog.show()
@ -132,10 +130,8 @@ class NodeConfigServiceDialog(Dialog):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
def click_save(self) -> None: def click_save(self) -> None:
self.canvas_node.core_node.config_services[:] = self.current_services self.node.config_services = self.current_services.copy()
logging.info( logging.info("saved node config services: %s", self.node.config_services)
"saved node config services: %s", self.canvas_node.core_node.config_services
)
self.destroy() self.destroy()
def click_cancel(self) -> None: def click_cancel(self) -> None:
@ -154,4 +150,4 @@ class NodeConfigServiceDialog(Dialog):
return return
def is_custom_service(self, service: str) -> bool: 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.dialogs.serviceconfig import ServiceConfigDialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.widgets import CheckboxList, ListboxScroll from core.gui.widgets import CheckboxList, ListboxScroll
from core.gui.wrappers import Node
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
from core.gui.graph.node import CanvasNode
class NodeServiceDialog(Dialog): class NodeServiceDialog(Dialog):
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: def __init__(self, app: "Application", node: Node) -> None:
title = f"{canvas_node.core_node.name} Services" title = f"{node.name} Services"
super().__init__(app, title) super().__init__(app, title)
self.canvas_node: "CanvasNode" = canvas_node self.node: Node = node
self.node_id: int = canvas_node.core_node.id
self.groups: Optional[ListboxScroll] = None self.groups: Optional[ListboxScroll] = None
self.services: Optional[CheckboxList] = None self.services: Optional[CheckboxList] = None
self.current: Optional[ListboxScroll] = None self.current: Optional[ListboxScroll] = None
services = set(canvas_node.core_node.services) services = set(node.services)
self.current_services: Set[str] = services self.current_services: Set[str] = services
self.draw() self.draw()
@ -38,31 +37,31 @@ class NodeServiceDialog(Dialog):
for i in range(3): for i in range(3):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) 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.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
self.groups = ListboxScroll(label_frame) self.groups = ListboxScroll(label_frame)
self.groups.grid(sticky="nsew") self.groups.grid(sticky=tk.NSEW)
for group in sorted(self.app.core.services): for group in sorted(self.app.core.services):
self.groups.listbox.insert(tk.END, group) self.groups.listbox.insert(tk.END, group)
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change) self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
self.groups.listbox.selection_set(0) self.groups.listbox.selection_set(0)
label_frame = ttk.LabelFrame(frame, text="Services") 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.columnconfigure(0, weight=1)
label_frame.rowconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1)
self.services = CheckboxList( self.services = CheckboxList(
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD 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 = 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.rowconfigure(0, weight=1)
label_frame.columnconfigure(0, weight=1) label_frame.columnconfigure(0, weight=1)
self.current = ListboxScroll(label_frame) self.current = ListboxScroll(label_frame)
self.current.grid(sticky="nsew") self.current.grid(sticky=tk.NSEW)
for service in sorted(self.current_services): for service in sorted(self.current_services):
self.current.listbox.insert(tk.END, service) self.current.listbox.insert(tk.END, service)
if self.is_custom_service(service): if self.is_custom_service(service):
@ -73,13 +72,13 @@ class NodeServiceDialog(Dialog):
for i in range(4): for i in range(4):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Configure", command=self.click_configure) 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 = 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 = 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 = 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 # trigger group change
self.handle_group_change() self.handle_group_change()
@ -104,7 +103,7 @@ class NodeServiceDialog(Dialog):
self.current.listbox.insert(tk.END, name) self.current.listbox.insert(tk.END, name)
if self.is_custom_service(name): if self.is_custom_service(name):
self.current.listbox.itemconfig(tk.END, bg="green") self.current.listbox.itemconfig(tk.END, bg="green")
self.canvas_node.core_node.services[:] = self.current_services self.node.services = self.current_services.copy()
def click_configure(self) -> None: def click_configure(self) -> None:
current_selection = self.current.listbox.curselection() current_selection = self.current.listbox.curselection()
@ -113,8 +112,7 @@ class NodeServiceDialog(Dialog):
self, self,
self.app, self.app,
self.current.listbox.get(current_selection[0]), self.current.listbox.get(current_selection[0]),
self.canvas_node, self.node,
self.node_id,
) )
# if error occurred when creating ServiceConfigDialog, don't show the dialog # if error occurred when creating ServiceConfigDialog, don't show the dialog
@ -128,8 +126,7 @@ class NodeServiceDialog(Dialog):
) )
def click_save(self) -> None: def click_save(self) -> None:
core_node = self.canvas_node.core_node self.node.services = self.current_services.copy()
core_node.services[:] = self.current_services
self.destroy() self.destroy()
def click_remove(self) -> None: def click_remove(self) -> None:
@ -144,6 +141,6 @@ class NodeServiceDialog(Dialog):
return return
def is_custom_service(self, service: str) -> bool: def is_custom_service(self, service: str) -> bool:
has_service_config = service in self.canvas_node.service_configs has_service_config = service in self.node.service_configs
has_file_config = service in self.canvas_node.service_file_configs has_file_config = service in self.node.service_file_configs
return has_service_config or has_file_config return has_service_config or has_file_config

View file

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

View file

@ -34,42 +34,42 @@ class PreferencesDialog(Dialog):
def draw_preferences(self) -> None: def draw_preferences(self) -> None:
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
frame.grid(sticky="nsew", pady=PADY) frame.grid(sticky=tk.NSEW, pady=PADY)
frame.columnconfigure(1, weight=1) frame.columnconfigure(1, weight=1)
label = ttk.Label(frame, text="Theme") 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() themes = self.app.style.theme_names()
combobox = ttk.Combobox( combobox = ttk.Combobox(
frame, textvariable=self.theme, values=themes, state="readonly" frame, textvariable=self.theme, values=themes, state="readonly"
) )
combobox.set(self.theme.get()) 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) combobox.bind("<<ComboboxSelected>>", self.theme_change)
label = ttk.Label(frame, text="Editor") 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( combobox = ttk.Combobox(
frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" 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 = 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()) terminals = sorted(appconfig.TERMINALS.values())
combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals) 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 = 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 = 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 = 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 = 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_frame.columnconfigure(0, weight=1)
scale = ttk.Scale( scale = ttk.Scale(
scale_frame, scale_frame,
@ -79,7 +79,7 @@ class PreferencesDialog(Dialog):
orient=tk.HORIZONTAL, orient=tk.HORIZONTAL,
variable=self.gui_scale, variable=self.gui_scale,
) )
scale.grid(row=0, column=0, sticky="ew") scale.grid(row=0, column=0, sticky=tk.EW)
entry = validation.AppScaleEntry( entry = validation.AppScaleEntry(
scale_frame, textvariable=self.gui_scale, width=4 scale_frame, textvariable=self.gui_scale, width=4
) )
@ -90,15 +90,15 @@ class PreferencesDialog(Dialog):
def draw_buttons(self) -> None: def draw_buttons(self) -> None:
frame = ttk.Frame(self.top) frame = ttk.Frame(self.top)
frame.grid(sticky="ew") frame.grid(sticky=tk.EW)
for i in range(2): for i in range(2):
frame.columnconfigure(i, weight=1) frame.columnconfigure(i, weight=1)
button = ttk.Button(frame, text="Save", command=self.click_save) 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 = 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: def theme_change(self, event: tk.Event) -> None:
theme = self.theme.get() 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 store all CORE nodes (nodes that execute commands) from all existing nodes
""" """
for nid, node in self.app.core.canvas_nodes.items(): for node in self.app.core.session.nodes.values():
if NodeUtils.is_container_node(node.core_node.type): if NodeUtils.is_container_node(node.type):
self.executable_nodes[node.core_node.name] = nid self.executable_nodes[node.name] = node.id
def draw(self) -> None: def draw(self) -> None:
self.top.rowconfigure(0, weight=1) self.top.rowconfigure(0, weight=1)
@ -38,56 +38,56 @@ class RunToolDialog(Dialog):
def draw_command_frame(self) -> None: def draw_command_frame(self) -> None:
# the main frame # the main frame
frame = ttk.Frame(self.top) 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.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1) frame.rowconfigure(1, weight=1)
labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD) 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.rowconfigure(0, weight=1)
labeled_frame.columnconfigure(0, weight=1) labeled_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(labeled_frame, textvariable=self.cmd) entry = ttk.Entry(labeled_frame, textvariable=self.cmd)
entry.grid(sticky="ew") entry.grid(sticky=tk.EW)
# results frame # results frame
labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD) 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.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1) labeled_frame.rowconfigure(0, weight=1)
self.result = CodeText(labeled_frame) self.result = CodeText(labeled_frame)
self.result.text.config(state=tk.DISABLED, height=15) 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 = ttk.Frame(labeled_frame)
button_frame.grid(sticky="nsew") button_frame.grid(sticky=tk.NSEW)
button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=1) button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="Run", command=self.click_run) 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 = 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: def draw_nodes_frame(self) -> None:
labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) 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.columnconfigure(0, weight=1)
labeled_frame.rowconfigure(0, weight=1) labeled_frame.rowconfigure(0, weight=1)
self.node_list = ListboxScroll(labeled_frame) self.node_list = ListboxScroll(labeled_frame)
self.node_list.listbox.config(selectmode=tk.MULTIPLE) 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()): for n in sorted(self.executable_nodes.keys()):
self.node_list.listbox.insert(tk.END, n) self.node_list.listbox.insert(tk.END, n)
button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD) 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(0, weight=1)
button_frame.columnconfigure(1, weight=1) button_frame.columnconfigure(1, weight=1)
button = ttk.Button(button_frame, text="All", command=self.click_all) 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 = 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: def click_all(self) -> None:
self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1) 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_name = self.node_list.listbox.get(selection)
node_id = self.executable_nodes[node_name] node_id = self.executable_nodes[node_name]
response = self.app.core.client.node_command( 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( self.result.text.insert(
tk.END, f"> {node_name} > {command}:\n{response.output}\n" tk.END, f"> {node_name} > {command}:\n{response.output}\n"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,14 +3,13 @@ import math
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING, Optional, Tuple from typing import TYPE_CHECKING, Optional, Tuple
from core.api.grpc import core_pb2
from core.api.grpc.core_pb2 import Interface, Link
from core.gui import themes from core.gui import themes
from core.gui.dialogs.linkconfig import LinkConfigurationDialog from core.gui.dialogs.linkconfig import LinkConfigurationDialog
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.nodeutils import NodeUtils from core.gui.nodeutils import NodeUtils
from core.gui.utils import bandwidth_text from core.gui.utils import bandwidth_text, delay_jitter_text
from core.gui.wrappers import Interface, Link
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.graph.graph import CanvasGraph from core.gui.graph.graph import CanvasGraph
@ -111,7 +110,9 @@ class Edge:
arc_y = (perp_m * arc_x) + b arc_y = (perp_m * arc_x) + b
return arc_x, arc_y 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) arc_pos = self._get_arcpoint(src_pos, dst_pos)
self.id = self.canvas.create_line( self.id = self.canvas.create_line(
*src_pos, *src_pos,
@ -121,6 +122,7 @@ class Edge:
tags=self.tag, tags=self.tag,
width=self.scaled_width(), width=self.scaled_width(),
fill=self.color, fill=self.color,
state=state,
) )
def redraw(self) -> None: def redraw(self) -> None:
@ -250,7 +252,7 @@ class CanvasWirelessEdge(Edge):
self.width: float = WIRELESS_WIDTH self.width: float = WIRELESS_WIDTH
color = link.color if link.color else WIRELESS_COLOR color = link.color if link.color else WIRELESS_COLOR
self.color: str = 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: if link.label:
self.middle_label_text(link.label) self.middle_label_text(link.label)
self.set_binding() self.set_binding()
@ -287,7 +289,7 @@ class CanvasEdge(Edge):
self.link: Optional[Link] = None self.link: Optional[Link] = None
self.asymmetric_link: Optional[Link] = None self.asymmetric_link: Optional[Link] = None
self.throughput: Optional[float] = None self.throughput: Optional[float] = None
self.draw(src_pos, dst_pos) self.draw(src_pos, dst_pos, tk.NORMAL)
self.set_binding() self.set_binding()
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
self.create_context() self.create_context()
@ -305,7 +307,7 @@ class CanvasEdge(Edge):
self.link = link self.link = link
self.draw_labels() self.draw_labels()
def iface_label(self, iface: core_pb2.Interface) -> str: def iface_label(self, iface: Interface) -> str:
label = "" label = ""
if iface.name and self.canvas.show_iface_names.get(): if iface.name and self.canvas.show_iface_names.get():
label = f"{iface.name}" label = f"{iface.name}"
@ -319,10 +321,10 @@ class CanvasEdge(Edge):
def create_node_labels(self) -> Tuple[str, str]: def create_node_labels(self) -> Tuple[str, str]:
label1 = None label1 = None
if self.link.HasField("iface1"): if self.link.iface1:
label1 = self.iface_label(self.link.iface1) label1 = self.iface_label(self.link.iface1)
label2 = None label2 = None
if self.link.HasField("iface2"): if self.link.iface2:
label2 = self.iface_label(self.link.iface2) label2 = self.iface_label(self.link.iface2)
return label1, label2 return label1, label2
@ -379,6 +381,7 @@ class CanvasEdge(Edge):
def check_wireless(self) -> None: def check_wireless(self) -> None:
if self.is_wireless(): if self.is_wireless():
self.canvas.itemconfig(self.id, state=tk.HIDDEN) self.canvas.itemconfig(self.id, state=tk.HIDDEN)
self.canvas.dtag(self.id, tags.EDGE)
self._check_antenna() self._check_antenna()
def _check_antenna(self) -> None: def _check_antenna(self) -> None:
@ -417,22 +420,38 @@ class CanvasEdge(Edge):
dialog.show() dialog.show()
def draw_link_options(self): def draw_link_options(self):
if not self.link.options:
return
options = self.link.options options = self.link.options
asym_options = None
if self.asymmetric_link and self.asymmetric_link.options:
asym_options = self.asymmetric_link.options
lines = [] lines = []
bandwidth = options.bandwidth # bandwidth
if bandwidth > 0: if options.bandwidth > 0:
lines.append(bandwidth_text(bandwidth)) bandwidth_line = bandwidth_text(options.bandwidth)
delay = options.delay if asym_options and asym_options.bandwidth > 0:
jitter = options.jitter bandwidth_line += f" / {bandwidth_text(asym_options.bandwidth)}"
if delay > 0 and jitter > 0: lines.append(bandwidth_line)
lines.append(f"{delay} us (\u00B1{jitter} us)") # delay/jitter
elif jitter > 0: dj_line = delay_jitter_text(options.delay, options.jitter)
lines.append(f"0 us (\u00B1{jitter} us)") if dj_line and asym_options:
loss = options.loss asym_dj_line = delay_jitter_text(asym_options.delay, asym_options.jitter)
if loss > 0: if asym_dj_line:
lines.append(f"loss={loss}%") dj_line += f" / {asym_dj_line}"
dup = options.dup if dj_line:
if dup > 0: lines.append(dj_line)
lines.append(f"dup={dup}%") # 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) label = "\n".join(lines)
self.middle_label_text(label) 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 import Image
from PIL.ImageTk import PhotoImage 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.dialogs.shapemod import ShapeDialog
from core.gui.graph import tags from core.gui.graph import tags
from core.gui.graph.edges import ( from core.gui.graph.edges import (
@ -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.graph.shapeutils import ShapeType, is_draw_shape, is_marker
from core.gui.images import ImageEnum, TypeToImage from core.gui.images import ImageEnum, TypeToImage
from core.gui.nodeutils import NodeDraw, NodeUtils from core.gui.nodeutils import NodeDraw, NodeUtils
from core.gui.wrappers import Interface, Link, LinkType, Node, Session, ThroughputsEvent
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -104,6 +97,8 @@ class CanvasGraph(tk.Canvas):
# drawing related # drawing related
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True) self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True) self.show_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_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
self.show_iface_names: BooleanVar = BooleanVar(value=False) self.show_iface_names: BooleanVar = BooleanVar(value=False)
@ -135,11 +130,6 @@ class CanvasGraph(tk.Canvas):
self.configure(scrollregion=self.bbox(tk.ALL)) self.configure(scrollregion=self.bbox(tk.ALL))
def reset_and_redraw(self, session: Session) -> None: 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 # reset view options to default state
self.show_node_labels.set(True) self.show_node_labels.set(True)
self.show_link_labels.set(True) self.show_link_labels.set(True)
@ -251,12 +241,12 @@ class CanvasGraph(tk.Canvas):
dst.edges.add(edge) dst.edges.add(edge)
self.edges[edge.token] = edge self.edges[edge.token] = edge
self.core.links[edge.token] = edge self.core.links[edge.token] = edge
if link.HasField("iface1"): if link.iface1:
iface1 = link.iface1 iface1 = link.iface1
self.core.iface_to_edge[(node1.id, iface1.id)] = token self.core.iface_to_edge[(node1.id, iface1.id)] = token
src.ifaces[iface1.id] = iface1 src.ifaces[iface1.id] = iface1
edge.src_iface = iface1 edge.src_iface = iface1
if link.HasField("iface2"): if link.iface2:
iface2 = link.iface2 iface2 = link.iface2
self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
dst.ifaces[iface2.id] = iface2 dst.ifaces[iface2.id] = iface2
@ -274,7 +264,7 @@ class CanvasGraph(tk.Canvas):
edge = self.edges.get(token) edge = self.edges.get(token)
if not edge: if not edge:
return 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: def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
network_id = link.network_id if link.network_id else None network_id = link.network_id if link.network_id else None
@ -323,10 +313,7 @@ class CanvasGraph(tk.Canvas):
edge.middle_label_text(link.label) edge.middle_label_text(link.label)
def add_core_node(self, core_node: Node) -> None: def add_core_node(self, core_node: Node) -> None:
if core_node.id in self.core.canvas_nodes: logging.debug("adding node: %s", core_node)
logging.error("core node already exists: %s", core_node)
return
logging.debug("adding node %s", core_node)
# if the gui can't find node's image, default to the "edit-node" image # 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) image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
if not image: if not image:
@ -335,24 +322,24 @@ class CanvasGraph(tk.Canvas):
y = core_node.position.y y = core_node.position.y
node = CanvasNode(self.app, x, y, core_node, image) node = CanvasNode(self.app, x, y, core_node, image)
self.nodes[node.id] = node self.nodes[node.id] = node
self.core.canvas_nodes[core_node.id] = node self.core.set_canvas_node(core_node, node)
def draw_session(self, session: Session) -> None: def draw_session(self, session: Session) -> None:
""" """
Draw existing session. Draw existing session.
""" """
# draw existing nodes # 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 # peer to peer node is not drawn on the GUI
if NodeUtils.is_ignore_node(core_node.type): if NodeUtils.is_ignore_node(core_node.type):
continue continue
self.add_core_node(core_node) self.add_core_node(core_node)
# draw existing links # draw existing links
for link in session.links: for link in session.links:
logging.debug("drawing link: %s", link) logging.debug("drawing link: %s", link)
canvas_node1 = self.core.canvas_nodes[link.node1_id] canvas_node1 = self.core.get_canvas_node(link.node1_id)
canvas_node2 = self.core.canvas_nodes[link.node2_id] canvas_node2 = self.core.get_canvas_node(link.node2_id)
if link.type == LinkType.WIRELESS: if link.type == LinkType.WIRELESS:
self.add_wireless_edge(canvas_node1, canvas_node2, link) self.add_wireless_edge(canvas_node1, canvas_node2, link)
else: else:
@ -555,8 +542,8 @@ class CanvasGraph(tk.Canvas):
shape.delete() shape.delete()
self.selection.clear() self.selection.clear()
self.core.deleted_graph_nodes(nodes) self.core.deleted_canvas_nodes(nodes)
self.core.deleted_graph_edges(edges) self.core.deleted_canvas_edges(edges)
def delete_edge(self, edge: CanvasEdge) -> None: def delete_edge(self, edge: CanvasEdge) -> None:
edge.delete() edge.delete()
@ -575,7 +562,7 @@ class CanvasGraph(tk.Canvas):
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type) dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
if dst_wireless: if dst_wireless:
src_node.delete_antenna() 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: def zoom(self, event: tk.Event, factor: float = None) -> None:
if not factor: if not factor:
@ -761,8 +748,8 @@ class CanvasGraph(tk.Canvas):
image_file = self.node_draw.image_file image_file = self.node_draw.image_file
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE) 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) 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.nodes[node.id] = node
self.core.set_canvas_node(core_node, node)
def width_and_height(self) -> Tuple[int, int]: 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 # maps original node canvas id to copy node canvas id
copy_map = {} copy_map = {}
# the edges that will be copy over # the edges that will be copy over
to_copy_edges = [] to_copy_edges = set()
to_copy_ids = {x.id for x in self.to_copy}
for canvas_node in self.to_copy: for canvas_node in self.to_copy:
core_node = canvas_node.core_node core_node = canvas_node.core_node
actual_x = core_node.position.x + 50 actual_x = core_node.position.x + 50
@ -951,30 +939,57 @@ class CanvasGraph(tk.Canvas):
if not copy: if not copy:
continue continue
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image) node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
# copy configurations and services # copy configurations and services
node.core_node.services[:] = canvas_node.core_node.services node.core_node.services = core_node.services.copy()
node.core_node.config_services[:] = canvas_node.core_node.config_services node.core_node.config_services = core_node.config_services.copy()
node.emane_model_configs = deepcopy(canvas_node.emane_model_configs) node.core_node.emane_model_configs = deepcopy(core_node.emane_model_configs)
node.wlan_config = deepcopy(canvas_node.wlan_config) node.core_node.wlan_config = deepcopy(core_node.wlan_config)
node.mobility_config = deepcopy(canvas_node.mobility_config) node.core_node.mobility_config = deepcopy(core_node.mobility_config)
node.service_configs = deepcopy(canvas_node.service_configs) node.core_node.service_configs = deepcopy(core_node.service_configs)
node.service_file_configs = deepcopy(canvas_node.service_file_configs) node.core_node.service_file_configs = deepcopy(
node.config_service_configs = deepcopy(canvas_node.config_service_configs) core_node.service_file_configs
)
node.core_node.config_service_configs = deepcopy(
core_node.config_service_configs
)
copy_map[canvas_node.id] = node.id copy_map[canvas_node.id] = node.id
self.core.canvas_nodes[copy.id] = node
self.nodes[node.id] = node self.nodes[node.id] = node
self.core.set_canvas_node(copy, node)
for edge in canvas_node.edges: 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: if canvas_node.id == edge.src:
dst_node = self.nodes[edge.dst] dst_node = self.nodes[edge.dst]
self.create_edge(node, dst_node) self.create_edge(node, dst_node)
token = create_edge_token(node.id, dst_node.id)
elif canvas_node.id == edge.dst: elif canvas_node.id == edge.dst:
src_node = self.nodes[edge.src] src_node = self.nodes[edge.src]
self.create_edge(src_node, node) 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: else:
to_copy_edges.append(edge) to_copy_edges.add(edge)
# copy link and link config # copy link and link config
for edge in to_copy_edges: 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) token = create_edge_token(src_node_copy.id, dst_node_copy.id)
copy_edge = self.edges[token] copy_edge = self.edges[token]
copy_link = copy_edge.link 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 options = edge.link.options
copy_link.options.CopyFrom(options) if options:
iface1_id = None copy_link.options = deepcopy(options)
if copy_link.HasField("iface1"): if options and options.unidirectional:
iface1_id = copy_link.iface1.id
iface2_id = None
if copy_link.HasField("iface2"):
iface2_id = copy_link.iface2.id
if not options.unidirectional:
copy_edge.asymmetric_link = None
else:
asym_iface1 = None asym_iface1 = None
if iface1_id: if iface1_id is not None:
asym_iface1 = Interface(id=iface1_id) asym_iface1 = Interface(id=iface1_id)
asym_iface2 = None asym_iface2 = None
if iface2_id: if iface2_id is not None:
asym_iface2 = Interface(id=iface2_id) asym_iface2 = Interface(id=iface2_id)
copy_edge.asymmetric_link = Link( copy_edge.asymmetric_link = Link(
node1_id=copy_link.node2_id, node1_id=copy_link.node2_id,
node2_id=copy_link.node1_id, node2_id=copy_link.node1_id,
iface1=asym_iface1, iface1=asym_iface2,
iface2=asym_iface2, iface2=asym_iface1,
options=edge.asymmetric_link.options, options=deepcopy(edge.asymmetric_link.options),
) )
copy_edge.redraw()
self.itemconfig( self.itemconfig(
copy_edge.id, copy_edge.id,
width=self.itemcget(edge.id, "width"), width=self.itemcget(edge.id, "width"),

View file

@ -1,15 +1,13 @@
import functools import functools
import logging import logging
import tkinter as tk 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 import grpc
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.common_pb2 import ConfigOption from core.gui import nodeutils, themes
from core.api.grpc.core_pb2 import Interface, Node, NodeType
from core.api.grpc.services_pb2 import NodeServiceData
from core.gui import themes
from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.emaneconfig import EmaneConfigDialog
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog 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 import tags
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
from core.gui.graph.tooltip import CanvasTooltip from core.gui.graph.tooltip import CanvasTooltip
from core.gui.images import ImageEnum from core.gui.images import ImageEnum, Images
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
from core.gui.wrappers import Interface, Node, NodeType
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -58,15 +57,6 @@ class CanvasNode:
self.wireless_edges: Set[CanvasWirelessEdge] = set() self.wireless_edges: Set[CanvasWirelessEdge] = set()
self.antennas: List[int] = [] self.antennas: List[int] = []
self.antenna_images: Dict[int, PhotoImage] = {} 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.setup_bindings()
self.context: tk.Menu = tk.Menu(self.canvas) self.context: tk.Menu = tk.Menu(self.canvas)
themes.style_menu(self.context) themes.style_menu(self.context)
@ -217,6 +207,7 @@ class CanvasNode:
self.context.delete(0, tk.END) self.context.delete(0, tk.END)
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
is_emane = self.core_node.type == NodeType.EMANE is_emane = self.core_node.type == NodeType.EMANE
is_mobility = is_wlan or is_emane
if self.app.core.is_runtime(): if self.app.core.is_runtime():
self.context.add_command(label="Configure", command=self.show_config) self.context.add_command(label="Configure", command=self.show_config)
if is_emane: if is_emane:
@ -227,7 +218,7 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config 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( self.context.add_command(
label="Mobility Player", command=self.show_mobility_player label="Mobility Player", command=self.show_mobility_player
) )
@ -246,6 +237,7 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config label="WLAN Config", command=self.show_wlan_config
) )
if is_mobility:
self.context.add_command( self.context.add_command(
label="Mobility Config", command=self.show_mobility_config label="Mobility Config", command=self.show_mobility_config
) )
@ -301,7 +293,7 @@ class CanvasNode:
dialog.show() dialog.show()
def show_mobility_config(self) -> None: def show_mobility_config(self) -> None:
dialog = MobilityConfigDialog(self.app, self) dialog = MobilityConfigDialog(self.app, self.core_node)
if not dialog.has_error: if not dialog.has_error:
dialog.show() dialog.show()
@ -310,15 +302,15 @@ class CanvasNode:
mobility_player.show() mobility_player.show()
def show_emane_config(self) -> None: def show_emane_config(self) -> None:
dialog = EmaneConfigDialog(self.app, self) dialog = EmaneConfigDialog(self.app, self.core_node)
dialog.show() dialog.show()
def show_services(self) -> None: def show_services(self) -> None:
dialog = NodeServiceDialog(self.app, self) dialog = NodeServiceDialog(self.app, self.core_node)
dialog.show() dialog.show()
def show_config_services(self) -> None: def show_config_services(self) -> None:
dialog = NodeConfigServiceDialog(self.app, self) dialog = NodeConfigServiceDialog(self.app, self.core_node)
dialog.show() dialog.show()
def has_emane_link(self, iface_id: int) -> Node: 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 dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
dy = node_y - int(23 * self.app.app_scale) - y dy = node_y - int(23 * self.app.app_scale) - y
self.canvas.move(antenna_id, dx, dy) 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 import Image
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import NodeType
from core.gui.appconfig import LOCAL_ICONS_PATH from core.gui.appconfig import LOCAL_ICONS_PATH
from core.gui.wrappers import NodeType
class Images: class Images:

View file

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

View file

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

View file

@ -3,9 +3,9 @@ from typing import List, Optional, Set
from PIL.ImageTk import PhotoImage from PIL.ImageTk import PhotoImage
from core.api.grpc.core_pb2 import Node, NodeType
from core.gui.appconfig import CustomNode, GuiConfig from core.gui.appconfig import CustomNode, GuiConfig
from core.gui.images import ImageEnum, Images, TypeToImage from core.gui.images import ImageEnum, Images, TypeToImage
from core.gui.wrappers import Node, NodeType
ICON_SIZE: int = 48 ICON_SIZE: int = 48
ANTENNA_SIZE: int = 32 ANTENNA_SIZE: int = 32
@ -17,7 +17,7 @@ class NodeDraw:
self.image: Optional[PhotoImage] = None self.image: Optional[PhotoImage] = None
self.image_enum: Optional[ImageEnum] = None self.image_enum: Optional[ImageEnum] = None
self.image_file: Optional[str] = None self.image_file: Optional[str] = None
self.node_type: NodeType = None self.node_type: Optional[NodeType] = None
self.model: Optional[str] = None self.model: Optional[str] = None
self.services: Set[str] = set() self.services: Set[str] = set()
self.label: Optional[str] = None self.label: Optional[str] = None
@ -63,10 +63,15 @@ class NodeUtils:
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
RJ45_NODES: Set[NodeType] = {NodeType.RJ45} RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} 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"} NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
ROUTER_NODES: Set[str] = {"router", "mdr"} ROUTER_NODES: Set[str] = {"router", "mdr"}
ANTENNA_ICON: PhotoImage = None ANTENNA_ICON: PhotoImage = None
@classmethod
def is_mobility(cls, node: Node) -> bool:
return node.type in cls.MOBILITY_NODES
@classmethod @classmethod
def is_router_node(cls, node: Node) -> bool: def is_router_node(cls, node: Node) -> bool:
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES 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 tkinter import ttk
from typing import TYPE_CHECKING, List, Optional 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.dialogs.alerts import AlertsDialog
from core.gui.themes import Styles from core.gui.themes import Styles
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application from core.gui.app import Application
@ -34,7 +34,7 @@ class StatusBar(ttk.Frame):
self.columnconfigure(3, weight=1) self.columnconfigure(3, weight=1)
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
frame.grid(row=0, column=0, sticky="ew") frame.grid(row=0, column=0, sticky=tk.EW)
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
self.status = ttk.Label( self.status = ttk.Label(
@ -44,22 +44,22 @@ class StatusBar(ttk.Frame):
borderwidth=1, borderwidth=1,
relief=tk.RIDGE, 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 = 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.set_zoom(self.app.canvas.ratio)
self.cpu_label = ttk.Label( self.cpu_label = ttk.Label(
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE 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.set_cpu(0.0)
self.alerts_button = ttk.Button( self.alerts_button = ttk.Button(
self, text="Alerts", command=self.click_alerts, style=self.alert_style 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: def set_cpu(self, usage: float) -> None:
self.cpu_label.config(text=f"CPU {usage * 100:.2f}%") 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: def add_alert(self, event: ExceptionEvent) -> None:
self.core_alarms.append(event) self.core_alarms.append(event)
level = event.exception_event.level level = event.level
self._set_alert_style(level) self._set_alert_style(level)
label = f"Alerts ({len(self.core_alarms)})" label = f"Alerts ({len(self.core_alarms)})"
self.alerts_button.config(text=label, style=self.alert_style) self.alerts_button.config(text=label, style=self.alert_style)

View file

@ -1,6 +1,7 @@
import logging import logging
import threading import threading
import time import time
import tkinter as tk
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,22 +27,20 @@ class ProgressTask:
self.time: Optional[float] = None self.time: Optional[float] = None
def start(self) -> 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.app.progress.start()
self.time = time.perf_counter() self.time = time.perf_counter()
thread = threading.Thread(target=self.run, daemon=True) thread = threading.Thread(target=self.run, daemon=True)
thread.start() thread.start()
def run(self) -> None: def run(self) -> None:
logging.info("running task")
try: try:
values = self.task(*self.args) values = self.task(*self.args)
if values is None: if values is None:
values = () values = ()
elif values and not isinstance(values, tuple): elif values is not None and not isinstance(values, tuple):
values = (values,) values = (values,)
if self.callback: if self.callback:
logging.info("calling callback")
self.app.after(0, self.callback, *values) self.app.after(0, self.callback, *values)
except Exception as e: except Exception as e:
logging.exception("progress task exception") 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 PIL.ImageTk import PhotoImage
from core.api.grpc import core_pb2
from core.gui.dialogs.colorpicker import ColorPickerDialog from core.gui.dialogs.colorpicker import ColorPickerDialog
from core.gui.dialogs.runtool import RunToolDialog from core.gui.dialogs.runtool import RunToolDialog
from core.gui.graph import tags from core.gui.graph import tags
@ -96,7 +95,7 @@ class ButtonBar(ttk.Frame):
image = self.app.get_icon(image_enum, TOOLBAR_SIZE) image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
button = ttk.Button(self, image=image, command=func) button = ttk.Button(self, image=image, command=func)
button.image = image button.image = image
button.grid(sticky="ew") button.grid(sticky=tk.EW)
Tooltip(button, tooltip) Tooltip(button, tooltip)
if radio: if radio:
self.radio_buttons.append(button) self.radio_buttons.append(button)
@ -125,7 +124,7 @@ class MarkerFrame(ttk.Frame):
image = self.app.get_icon(ImageEnum.DELETE, 16) image = self.app.get_icon(ImageEnum.DELETE, 16)
button = ttk.Button(self, image=image, width=2, command=self.click_clear) button = ttk.Button(self, image=image, width=2, command=self.click_clear)
button.image = image button.image = image
button.grid(sticky="ew", pady=self.PAD) button.grid(sticky=tk.EW, pady=self.PAD)
Tooltip(button, "Delete Marker") Tooltip(button, "Delete Marker")
sizes = [1, 3, 8, 10] sizes = [1, 3, 8, 10]
@ -133,14 +132,14 @@ class MarkerFrame(ttk.Frame):
sizes = ttk.Combobox( sizes = ttk.Combobox(
self, state="readonly", textvariable=self.size, value=sizes, width=2 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") Tooltip(sizes, "Marker Size")
frame_size = TOOLBAR_SIZE frame_size = TOOLBAR_SIZE
self.color_frame = tk.Frame( self.color_frame = tk.Frame(
self, background=self.color, height=frame_size, width=frame_size 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) self.color_frame.bind("<Button-1>", self.click_color)
Tooltip(self.color_frame, "Marker Color") Tooltip(self.color_frame, "Marker Color")
@ -208,7 +207,7 @@ class Toolbar(ttk.Frame):
def draw_design_frame(self) -> None: def draw_design_frame(self) -> None:
self.design_frame = ButtonBar(self, self.app) 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.design_frame.columnconfigure(0, weight=1)
self.play_button = self.design_frame.create_button( self.play_button = self.design_frame.create_button(
ImageEnum.START, self.click_start, "Start Session" ImageEnum.START, self.click_start, "Start Session"
@ -240,7 +239,7 @@ class Toolbar(ttk.Frame):
def draw_runtime_frame(self) -> None: def draw_runtime_frame(self) -> None:
self.runtime_frame = ButtonBar(self, self.app) 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.runtime_frame.columnconfigure(0, weight=1)
self.stop_button = self.runtime_frame.create_button( self.stop_button = self.runtime_frame.create_button(
ImageEnum.STOP, self.click_stop, "Stop Session" ImageEnum.STOP, self.click_stop, "Stop Session"
@ -300,15 +299,15 @@ class Toolbar(ttk.Frame):
) )
task.start() task.start()
def start_callback(self, response: core_pb2.StartSessionResponse) -> None: def start_callback(self, result: bool, exceptions: List[str]) -> None:
if response.result: if result:
self.set_runtime() self.set_runtime()
self.app.core.set_metadata() self.app.core.set_metadata()
self.app.core.show_mobility_players() self.app.core.show_mobility_players()
else: else:
enable_buttons(self.design_frame, enabled=True) enable_buttons(self.design_frame, enabled=True)
if response.exceptions: if exceptions:
message = "\n".join(response.exceptions) message = "\n".join(exceptions)
self.app.show_error("Start Session Error", message) self.app.show_error("Start Session Error", message)
def set_runtime(self) -> None: def set_runtime(self) -> None:
@ -388,7 +387,7 @@ class Toolbar(ttk.Frame):
self.runtime_frame, image=image, direction=tk.RIGHT self.runtime_frame, image=image, direction=tk.RIGHT
) )
menu_button.image = image menu_button.image = image
menu_button.grid(sticky="ew") menu_button.grid(sticky=tk.EW)
self.observers_menu = ObserversMenu(menu_button, self.app) self.observers_menu = ObserversMenu(menu_button, self.app)
menu_button["menu"] = self.observers_menu menu_button["menu"] = self.observers_menu
@ -405,7 +404,7 @@ class Toolbar(ttk.Frame):
) )
task.start() task.start()
def stop_callback(self, response: core_pb2.StopSessionResponse) -> None: def stop_callback(self, result: bool) -> None:
self.set_design() self.set_design()
self.app.canvas.stopped_session() self.app.canvas.stopped_session()

View file

@ -46,7 +46,7 @@ class Tooltip(object):
self.tw.rowconfigure(0, weight=1) self.tw.rowconfigure(0, weight=1)
self.tw.columnconfigure(0, weight=1) self.tw.columnconfigure(0, weight=1)
frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) 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 = ttk.Label(frame, text=self.text, style=Styles.tooltip)
label.grid() label.grid()

View file

@ -1,3 +1,6 @@
from typing import Optional
def bandwidth_text(bandwidth: int) -> str: def bandwidth_text(bandwidth: int) -> str:
size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"} size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"}
unit = 1000 unit = 1000
@ -8,3 +11,12 @@ def bandwidth_text(bandwidth: int) -> str:
if i == 3: if i == 3:
break break
return f"{bandwidth} {size[i]}" 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 tkinter import filedialog, font, ttk
from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type
from core.api.grpc import core_pb2 from core.gui import appconfig, themes, validation
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.dialogs.dialog import Dialog from core.gui.dialogs.dialog import Dialog
from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.themes import FRAME_PAD, PADX, PADY
from core.gui.wrappers import ConfigOption, ConfigOptionType
if TYPE_CHECKING: if TYPE_CHECKING:
from core.gui.app import Application 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: 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: if file_path:
value.set(file_path) value.set(file_path)
@ -47,13 +47,13 @@ class FrameScroll(ttk.Frame):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
bg = self.app.style.lookup(".", "background") bg = self.app.style.lookup(".", "background")
self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg) 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.columnconfigure(0, weight=1)
self.canvas.rowconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1)
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar( self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(
self, orient="vertical", command=self.canvas.yview 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: ttk.Frame = _cls(self.canvas)
self.frame_id: int = self.canvas.create_window( self.frame_id: int = self.canvas.create_window(
0, 0, anchor="nw", window=self.frame 0, 0, anchor="nw", window=self.frame
@ -108,15 +108,15 @@ class ConfigFrame(ttk.Notebook):
self.add(tab, text=group_name) self.add(tab, text=group_name)
for index, option in enumerate(sorted(group, key=lambda x: x.name)): for index, option in enumerate(sorted(group, key=lambda x: x.name)):
label = ttk.Label(tab.frame, text=option.label) 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() value = tk.StringVar()
if option.type == core_pb2.ConfigOptionType.BOOL: if option.type == ConfigOptionType.BOOL:
select = ("On", "Off") select = ("On", "Off")
state = "readonly" if self.enabled else tk.DISABLED state = "readonly" if self.enabled else tk.DISABLED
combobox = ttk.Combobox( combobox = ttk.Combobox(
tab.frame, textvariable=value, values=select, state=state 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": if option.value == "1":
value.set("On") value.set("On")
else: else:
@ -128,16 +128,16 @@ class ConfigFrame(ttk.Notebook):
combobox = ttk.Combobox( combobox = ttk.Combobox(
tab.frame, textvariable=value, values=select, state=state 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)
elif option.type == core_pb2.ConfigOptionType.STRING: elif option.type == ConfigOptionType.STRING:
value.set(option.value) value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED state = tk.NORMAL if self.enabled else tk.DISABLED
if "file" in option.label: if "file" in option.label:
file_frame = ttk.Frame(tab.frame) 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) file_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(file_frame, textvariable=value, state=state) 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) func = partial(file_button_click, value, self)
button = ttk.Button( button = ttk.Button(
file_frame, text="...", command=func, state=state file_frame, text="...", command=func, state=state
@ -145,21 +145,21 @@ class ConfigFrame(ttk.Notebook):
button.grid(row=0, column=1) button.grid(row=0, column=1)
else: else:
entry = ttk.Entry(tab.frame, textvariable=value, state=state) 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: elif option.type in INT_TYPES:
value.set(option.value) value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED state = tk.NORMAL if self.enabled else tk.DISABLED
entry = validation.PositiveIntEntry( entry = validation.PositiveIntEntry(
tab.frame, textvariable=value, state=state 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 == core_pb2.ConfigOptionType.FLOAT: elif option.type == ConfigOptionType.FLOAT:
value.set(option.value) value.set(option.value)
state = tk.NORMAL if self.enabled else tk.DISABLED state = tk.NORMAL if self.enabled else tk.DISABLED
entry = validation.PositiveFloatEntry( entry = validation.PositiveFloatEntry(
tab.frame, textvariable=value, state=state tab.frame, textvariable=value, state=state
) )
entry.grid(row=index, column=1, sticky="ew") entry.grid(row=index, column=1, sticky=tk.EW)
else: else:
logging.error("unhandled config option type: %s", option.type) logging.error("unhandled config option type: %s", option.type)
self.values[option.name] = value self.values[option.name] = value
@ -169,7 +169,7 @@ class ConfigFrame(ttk.Notebook):
option = self.config[key] option = self.config[key]
value = self.values[key] value = self.values[key]
config_value = value.get() config_value = value.get()
if option.type == core_pb2.ConfigOptionType.BOOL: if option.type == ConfigOptionType.BOOL:
if config_value == "On": if config_value == "On":
option.value = "1" option.value = "1"
else: else:
@ -182,7 +182,7 @@ class ConfigFrame(ttk.Notebook):
for name, data in config.items(): for name, data in config.items():
option = self.config[name] option = self.config[name]
value = self.values[name] value = self.values[name]
if option.type == core_pb2.ConfigOptionType.BOOL: if option.type == ConfigOptionType.BOOL:
if data == "1": if data == "1":
data = "On" data = "On"
else: else:
@ -196,7 +196,7 @@ class ListboxScroll(ttk.Frame):
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) 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.listbox: tk.Listbox = tk.Listbox(
self, self,
selectmode=tk.BROWSE, selectmode=tk.BROWSE,
@ -204,7 +204,7 @@ class ListboxScroll(ttk.Frame):
exportselection=False, exportselection=False,
) )
themes.style_listbox(self.listbox) themes.style_listbox(self.listbox)
self.listbox.grid(row=0, column=0, sticky="nsew") self.listbox.grid(row=0, column=0, sticky=tk.NSEW)
self.scrollbar.config(command=self.listbox.yview) self.scrollbar.config(command=self.listbox.yview)
@ -224,7 +224,7 @@ class CheckboxList(FrameScroll):
var = tk.BooleanVar(value=checked) var = tk.BooleanVar(value=checked)
func = partial(self.clicked, name, var) func = partial(self.clicked, name, var)
checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
checkbox.grid(sticky="w") checkbox.grid(sticky=tk.W)
class CodeFont(font.Font): class CodeFont(font.Font):
@ -250,9 +250,9 @@ class CodeText(ttk.Frame):
selectforeground="black", selectforeground="black",
relief=tk.FLAT, 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 = 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) 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 import time
from functools import total_ordering from functools import total_ordering
from pathlib import Path 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 import utils
from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager 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.data import EventData, LinkData, LinkOptions
from core.emulator.enumerations import ( from core.emulator.enumerations import (
ConfigDataTypes, ConfigDataTypes,
@ -31,6 +32,13 @@ if TYPE_CHECKING:
from core.emulator.session import Session 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): class MobilityManager(ModelManager):
""" """
Member of session class for handling configuration data for mobility and Member of session class for handling configuration data for mobility and
@ -69,35 +77,30 @@ class MobilityManager(ModelManager):
""" """
if node_ids is None: if node_ids is None:
node_ids = self.nodes() node_ids = self.nodes()
for node_id in node_ids: for node_id in node_ids:
logging.debug("checking mobility startup for node: %s", node_id)
logging.debug( 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: 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: except CoreError:
logging.exception("mobility startup error")
logging.warning( logging.warning(
"skipping mobility configuration for unknown node: %s", node_id "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: def handleevent(self, event_data: EventData) -> None:
""" """
Handle an Event Message used to start, stop, or pause 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 :param event_data: event data to handle
:return: nothing :return: nothing
@ -106,40 +109,35 @@ class MobilityManager(ModelManager):
node_id = event_data.node node_id = event_data.node
name = event_data.name name = event_data.name
try: try:
node = self.session.get_node(node_id, WlanNode) node = get_mobility_node(self.session, node_id)
except CoreError: except CoreError:
logging.exception( 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 return
# name is e.g. "mobility:ns2script" # name is e.g. "mobility:ns2script"
models = name[9:].split(",") models = name[9:].split(",")
for model in models: for model in models:
try: cls = self.models.get(model)
cls = self.models[model] if not cls:
except KeyError: logging.warning("ignoring event for unknown model '%s'", model)
logging.warning("Ignoring event for unknown model '%s'", model)
continue continue
if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]: if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]:
model = node.mobility model = node.mobility
else: else:
continue continue
if model is None: 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 continue
if cls.name != model.name: if cls.name != model.name:
logging.warning( logging.warning(
"Ignoring event for %s wrong model %s,%s", "ignoring event for %s wrong model %s,%s",
node.name, node.name,
cls.name, cls.name,
model.name, model.name,
) )
continue continue
if event_type in [EventTypes.STOP, EventTypes.RESTART]: if event_type in [EventTypes.STOP, EventTypes.RESTART]:
model.stop(move_initial=True) model.stop(move_initial=True)
if event_type in [EventTypes.START, EventTypes.RESTART]: if event_type in [EventTypes.START, EventTypes.RESTART]:
@ -162,11 +160,9 @@ class MobilityManager(ModelManager):
event_type = EventTypes.START event_type = EventTypes.START
elif model.state == model.STATE_PAUSED: elif model.state == model.STATE_PAUSED:
event_type = EventTypes.PAUSE event_type = EventTypes.PAUSE
start_time = int(model.lasttime - model.timezero) start_time = int(model.lasttime - model.timezero)
end_time = int(model.endtime) end_time = int(model.endtime)
data = f"start={start_time} end={end_time}" data = f"start={start_time} end={end_time}"
event_data = EventData( event_data = EventData(
node=model.id, node=model.id,
event_type=event_type, event_type=event_type,
@ -174,15 +170,14 @@ class MobilityManager(ModelManager):
data=data, data=data,
time=str(time.monotonic()), time=str(time.monotonic()),
) )
self.session.broadcast_event(event_data) self.session.broadcast_event(event_data)
def updatewlans( def update_nets(
self, moved: List[CoreNode], moved_ifaces: List[CoreInterface] self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]
) -> None: ) -> None:
""" """
A mobility script has caused nodes in the 'moved' list to move. 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. were to recalculate for each individual node movement.
:param moved: moved nodes :param moved: moved nodes
@ -191,11 +186,11 @@ class MobilityManager(ModelManager):
""" """
for node_id in self.nodes(): for node_id in self.nodes():
try: try:
node = self.session.get_node(node_id, WlanNode) node = get_mobility_node(self.session, node_id)
except CoreError:
continue
if node.model: if node.model:
node.model.update(moved, moved_ifaces) node.model.update(moved, moved_ifaces)
except CoreError:
logging.exception("error updating mobility node")
class WirelessModel(ConfigurableOptions): class WirelessModel(ConfigurableOptions):
@ -593,7 +588,7 @@ class WayPointMobility(WirelessModel):
self.lasttime: Optional[float] = None self.lasttime: Optional[float] = None
self.endtime: Optional[int] = None self.endtime: Optional[int] = None
self.timezero: float = 0.0 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 # these are really set in child class via confmatrix
self.loop: bool = False self.loop: bool = False
self.refresh_ms: int = 50 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) # (ns-3 sets this to False as new waypoints may be added from trace)
self.empty_queue_stop: bool = True self.empty_queue_stop: bool = True
def startup(self):
raise NotImplementedError
def runround(self) -> None: def runround(self) -> None:
""" """
Advance script time and move nodes. 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? # only move interfaces attached to self.wlan, or all nodenum in script?
moved = [] moved = []
moved_ifaces = [] moved_ifaces = []
for iface in self.wlan.get_ifaces(): for iface in self.net.get_ifaces():
node = iface.node node = iface.node
if self.movenode(node, dt): if self.movenode(node, dt):
moved.append(node) moved.append(node)
moved_ifaces.append(iface) moved_ifaces.append(iface)
# calculate all ranges after moving nodes; this saves calculations # 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 # TODO: check session state
self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
@ -723,7 +721,7 @@ class WayPointMobility(WirelessModel):
""" """
moved = [] moved = []
moved_ifaces = [] moved_ifaces = []
for iface in self.wlan.get_ifaces(): for iface in self.net.get_ifaces():
node = iface.node node = iface.node
if node.id not in self.initial: if node.id not in self.initial:
continue continue
@ -731,7 +729,7 @@ class WayPointMobility(WirelessModel):
self.setnodeposition(node, x, y, z) self.setnodeposition(node, x, y, z)
moved.append(node) moved.append(node)
moved_ifaces.append(iface) moved_ifaces.append(iface)
self.session.mobility.updatewlans(moved, moved_ifaces) self.session.mobility.update_nets(moved, moved_ifaces)
def addwaypoint( def addwaypoint(
self, self,
@ -1094,7 +1092,7 @@ class Ns2ScriptedMobility(WayPointMobility):
:return: nothing :return: nothing
""" """
if self.autostart == "": 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 return
try: try:
t = float(self.autostart) t = float(self.autostart)
@ -1102,11 +1100,11 @@ class Ns2ScriptedMobility(WayPointMobility):
logging.exception( logging.exception(
"Invalid auto-start seconds specified '%s' for %s", "Invalid auto-start seconds specified '%s' for %s",
self.autostart, self.autostart,
self.wlan.name, self.net.name,
) )
return return
self.movenodesinitial() 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.state = self.STATE_RUNNING
self.session.event_loop.add_event(t, self.run) self.session.event_loop.add_event(t, self.run)

View file

@ -10,7 +10,17 @@ services.
import enum import enum
import logging import logging
import time 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 import utils
from core.emulator.data import FileData from core.emulator.data import FileData
@ -21,6 +31,8 @@ from core.nodes.base import CoreNode
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
CoreServiceType = Union["CoreService", Type["CoreService"]]
class ServiceBootError(Exception): class ServiceBootError(Exception):
pass pass
@ -39,95 +51,56 @@ class ServiceDependencies:
provided. provided.
""" """
def __init__(self, services: List["CoreService"]) -> None: def __init__(self, services: List["CoreServiceType"]) -> 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"] = []
self.visited: Set[str] = set() 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"]]: def _search(
""" self,
Generates the boot paths for the services provided to the class. 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 def boot_order(self) -> List[List["CoreServiceType"]]:
""" for service in self.roots:
paths = [] self._search(service)
for name in self.node_services: return self.boot_paths
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
class ServiceShim: class ServiceShim:
@ -470,23 +443,16 @@ class CoreServices:
:param node: node to start services on :param node: node to start services on
:return: nothing :return: nothing
""" """
boot_paths = ServiceDependencies(node.services).boot_paths() boot_paths = ServiceDependencies(node.services).boot_order()
funcs = [] funcs = []
for boot_path in boot_paths: for boot_path in boot_paths:
args = (node, boot_path) args = (node, boot_path)
funcs.append((self._start_boot_paths, args, {})) funcs.append((self._boot_service_path, args, {}))
result, exceptions = utils.threadpool(funcs) result, exceptions = utils.threadpool(funcs)
if exceptions: if exceptions:
raise ServiceBootError(*exceptions) raise ServiceBootError(*exceptions)
def _start_boot_paths(self, node: CoreNode, boot_path: List["CoreService"]) -> None: def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]):
"""
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
"""
logging.info( logging.info(
"booting node(%s) services: %s", "booting node(%s) services: %s",
node.name, node.name,
@ -496,11 +462,11 @@ class CoreServices:
service = self.get_service(node.id, service.name, default_service=True) service = self.get_service(node.id, service.name, default_service=True)
try: try:
self.boot_service(node, service) self.boot_service(node, service)
except Exception: except Exception as e:
logging.exception("exception booting service: %s", service.name) 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 Start a service on a node. Create private dirs, generate config
files, and execute startup commands. files, and execute startup commands.
@ -584,7 +550,7 @@ class CoreServices:
return True return True
return False 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. Run the validation command(s) for a service.
@ -622,7 +588,7 @@ class CoreServices:
for service in node.services: for service in node.services:
self.stop_service(node, service) 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. Stop a service on a node.
@ -724,7 +690,7 @@ class CoreServices:
service.config_data[file_name] = data service.config_data[file_name] = data
def startup_service( def startup_service(
self, node: CoreNode, service: "CoreService", wait: bool = False self, node: CoreNode, service: "CoreServiceType", wait: bool = False
) -> int: ) -> int:
""" """
Startup a node service. Startup a node service.
@ -747,7 +713,7 @@ class CoreServices:
status = -1 status = -1
return status 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. Creates node service files.

211
daemon/poetry.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import itertools
import os import os
import pytest import pytest
@ -15,40 +16,6 @@ SERVICE_ONE = "MyService"
SERVICE_TWO = "MyService2" 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: class TestServices:
def test_service_all_files(self, session: Session): def test_service_all_files(self, session: Session):
# given # given
@ -253,37 +220,157 @@ class TestServices:
assert default_service == my_service assert default_service == my_service
assert custom_service and custom_service != my_service assert custom_service and custom_service != my_service
def test_services_dependencies(self): def test_services_dependency(self):
# given # 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 # 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})"
)
def test_services_dependency_missing(self):
# given
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
permutations = itertools.permutations(services)
for permutation in permutations:
permutation = list(permutation)
with pytest.raises(ValueError):
ServiceDependencies(permutation).boot_order()
def test_services_dependency_cycle(self):
# given
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
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 # then
assert len(boot_paths) == 2 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_dependencies_not_present(self): def test_services_dependency_common2(self):
# given # given
services = [ service_a = CoreService()
ServiceA, service_a.name = "a"
ServiceB, service_b = CoreService()
ServiceC, service_b.name = "b"
ServiceD, service_c = CoreService()
ServiceF, service_c.name = "c"
ServiceBadDependency, 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, then # when
with pytest.raises(ValueError): permutations = itertools.permutations(services)
ServiceDependencies(services).boot_paths() for permutation in permutations:
permutation = list(permutation)
results = ServiceDependencies(permutation).boot_order()
def test_services_dependencies_cycle(self): # then
# given for result in results:
service_d = ServiceD() assert len(result) == 4
service_d.dependencies = ("C",) result_set = {x.name for x in result}
services = [ServiceA, ServiceB, ServiceC, service_d, ServiceF] a_index = result.index(service_a)
b_index = result.index(service_b)
# when, then c_index = result.index(service_c)
with pytest.raises(ValueError): d_index = result.index(service_d)
ServiceDependencies(services).boot_paths() 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 as they are moved in the EMANE emulation. This would occur when an Emulation
Script Generator, for example, is running a mobility script. Script Generator, for example, is running a mobility script.
## EMANE Installation
EMANE can be installed from deb or RPM packages or from source. See the
[EMANE GitHub](https://github.com/adjacentlink/emane) for full details.
Here are quick instructions for installing all EMANE packages for Ubuntu 18.04:
```shell
# install dependencies
sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl
wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
# install base emane packages
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb
# install python3 bindings
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb
```
## EMANE Configuration ## EMANE Configuration
The CORE configuration file **/etc/core/core.conf** has options specific to The CORE configuration file **/etc/core/core.conf** has options specific to

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 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 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 > **NOTE:** the following assumes CORE has been installed successfully
There is an invoke task to help with this case.
```shell ```shell
cd <CORE_REPO> core-python <script>
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
``` ```
## Manually Install EMANE ## Manually Install EMANE
@ -199,7 +183,6 @@ Available tasks:
install-emane install emane and the python bindings install-emane install emane and the python bindings
install-scripts install core script files, modified to leverage virtual environment install-scripts install core script files, modified to leverage virtual environment
install-service install systemd core service install-service install systemd core service
run runs a user script in the core virtual environment
test run core tests test run core tests
test-emane run core emane tests test-emane run core emane tests
test-mock run core tests using mock to avoid running as sudo 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: else:
c.run(f"sudo cp {script} {dest}", hide=hide) 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 # install core configuration file
config_dir = "/etc/core" config_dir = "/etc/core"
c.run(f"sudo mkdir -p {config_dir}", hide=hide) 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) install_service(c, hide, prefix)
with p.start("installing ospf mdr"): with p.start("installing ospf mdr"):
install_ospf_mdr(c, os_info, hide) 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( @task(
@ -402,6 +414,10 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX):
dest = bin_dir.joinpath(script.name) dest = bin_dir.joinpath(script.name)
c.run(f"sudo rm -f {dest}", hide=hide) 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 # install service
systemd_dir = Path("/lib/systemd/system/") systemd_dir = Path("/lib/systemd/system/")
service_name = "core-daemon.service" 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 @task
def test(c): def test(c):
""" """