commit
87ca431e73
72 changed files with 2542 additions and 1829 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,3 +1,30 @@
|
|||
## 2020-08-21 CORE 7.1.0
|
||||
|
||||
* Installation
|
||||
* added core-python script that gets installed to help globally reference the virtual environment
|
||||
* gRPC API
|
||||
* GetSession will now return all configuration information for a session and the file it was opened from, if applicable
|
||||
* node update events will now include icon information
|
||||
* fixed issue with getting session throughputs for sessions with a high id
|
||||
* core-daemon
|
||||
* \#503 - EMANE networks will now work with mobility again
|
||||
* \#506 - fixed service dependency resolution issue
|
||||
* fixed issue sending hooks to core-gui when joining session
|
||||
* core-pygui
|
||||
* fixed issues editing hooks
|
||||
* fixed issue with cpu usage when joining a session
|
||||
* fixed mac field not being disabled during runtime when configuring a node
|
||||
* removed unlimited button from link config dialog
|
||||
* fixed issue with copy/paste links and their options
|
||||
* fixed issue with adding nodes/links and editing links during runtime
|
||||
* updated open file dialog in config dialogs to open to ~/.coregui home directory
|
||||
* fixed issue double clicking sessions dialog in invalid areas
|
||||
* added display of asymmetric link options on links
|
||||
* fixed emane config dialog display
|
||||
* fixed issue saving backgrounds in xml files
|
||||
* added view toggle for wired/wireless links
|
||||
* node events will now update icons
|
||||
|
||||
## 2020-07-28 CORE 7.0.1
|
||||
|
||||
* Bugfixes
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
# this defines the CORE version number, must be static for AC_INIT
|
||||
AC_INIT(core, 7.0.1)
|
||||
AC_INIT(core, 7.1.0)
|
||||
|
||||
# autoconf and automake initialization
|
||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||
|
|
|
@ -552,11 +552,12 @@ class CoreGrpcClient:
|
|||
source: str = None,
|
||||
) -> core_pb2.EditNodeResponse:
|
||||
"""
|
||||
Edit a node, currently only changes position.
|
||||
Edit a node's icon and/or location, can only use position(x,y) or
|
||||
geo(lon, lat, alt), not both.
|
||||
|
||||
:param session_id: session id
|
||||
:param node_id: node id
|
||||
:param position: position to set node to
|
||||
:param position: x,y location for node
|
||||
:param icon: path to icon for gui to use for node
|
||||
:param geo: lon,lat,alt location for node
|
||||
:param source: application source
|
||||
|
|
|
@ -32,6 +32,7 @@ def handle_node_event(node_data: NodeData) -> core_pb2.Event:
|
|||
id=node.id,
|
||||
name=node.name,
|
||||
model=node.type,
|
||||
icon=node.icon,
|
||||
position=position,
|
||||
geo=geo,
|
||||
services=services,
|
||||
|
|
|
@ -8,14 +8,25 @@ from grpc import ServicerContext
|
|||
|
||||
from core import utils
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
|
||||
from core.api.grpc.common_pb2 import MappedConfig
|
||||
from core.api.grpc.configservices_pb2 import ConfigServiceConfig
|
||||
from core.api.grpc.emane_pb2 import GetEmaneModelConfig
|
||||
from core.api.grpc.services_pb2 import (
|
||||
NodeServiceConfig,
|
||||
NodeServiceData,
|
||||
ServiceConfig,
|
||||
ServiceDefaults,
|
||||
)
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.emulator.session import Session
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
from core.errors import CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
WORKERS = 10
|
||||
|
@ -218,7 +229,7 @@ def get_config_options(
|
|||
"""
|
||||
results = {}
|
||||
for configuration in configurable_options.configurations():
|
||||
value = config[configuration.id]
|
||||
value = config.get(configuration.id, configuration.default)
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=configuration.label,
|
||||
name=configuration.id,
|
||||
|
@ -536,3 +547,131 @@ def get_nem_id(
|
|||
message = f"{node.name} interface {iface_id} nem id does not exist"
|
||||
context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
|
||||
return nem_id
|
||||
|
||||
|
||||
def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]:
|
||||
configs = []
|
||||
for _id in session.emane.node_configurations:
|
||||
if _id == -1:
|
||||
continue
|
||||
model_configs = session.emane.node_configurations[_id]
|
||||
for model_name in model_configs:
|
||||
model = session.emane.models[model_name]
|
||||
current_config = session.emane.get_model_config(_id, model_name)
|
||||
config = get_config_options(current_config, model)
|
||||
node_id, iface_id = parse_emane_model_id(_id)
|
||||
model_config = GetEmaneModelConfig(
|
||||
node_id=node_id, model=model_name, iface_id=iface_id, config=config
|
||||
)
|
||||
configs.append(model_config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]:
|
||||
configs = {}
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != BasicRangeModel.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, BasicRangeModel)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
configs[node_id] = mapped_config
|
||||
return configs
|
||||
|
||||
|
||||
def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]:
|
||||
configs = {}
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != Ns2ScriptedMobility.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, Ns2ScriptedMobility)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
configs[node_id] = mapped_config
|
||||
return configs
|
||||
|
||||
|
||||
def get_hooks(session: Session) -> List[core_pb2.Hook]:
|
||||
hooks = []
|
||||
for state in session.hooks:
|
||||
state_hooks = session.hooks[state]
|
||||
for file_name, file_data in state_hooks:
|
||||
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
|
||||
hooks.append(hook)
|
||||
return hooks
|
||||
|
||||
|
||||
def get_emane_models(session: Session) -> List[str]:
|
||||
emane_models = []
|
||||
for model in session.emane.models.keys():
|
||||
if len(model.split("_")) != 2:
|
||||
continue
|
||||
emane_models.append(model)
|
||||
return emane_models
|
||||
|
||||
|
||||
def get_default_services(session: Session) -> List[ServiceDefaults]:
|
||||
default_services = []
|
||||
for name, services in session.services.default_services.items():
|
||||
default_service = ServiceDefaults(node_type=name, services=services)
|
||||
default_services.append(default_service)
|
||||
return default_services
|
||||
|
||||
|
||||
def get_node_service_configs(session: Session) -> List[NodeServiceConfig]:
|
||||
configs = []
|
||||
for node_id, service_configs in session.services.custom_services.items():
|
||||
for name in service_configs:
|
||||
service = session.services.get_service(node_id, name)
|
||||
service_proto = get_service_configuration(service)
|
||||
config = NodeServiceConfig(
|
||||
node_id=node_id,
|
||||
service=name,
|
||||
data=service_proto,
|
||||
files=service.config_data,
|
||||
)
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
|
||||
def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]:
|
||||
configs = []
|
||||
for node in session.nodes.values():
|
||||
if not isinstance(node, CoreNodeBase):
|
||||
continue
|
||||
for name, service in node.config_services.items():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_proto = ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
|
||||
def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]:
|
||||
current_config = session.emane.get_configs()
|
||||
return get_config_options(current_config, session.emane.emane_config)
|
||||
|
||||
|
||||
def get_mobility_node(
|
||||
session: Session, node_id: int, context: ServicerContext
|
||||
) -> Union[WlanNode, EmaneNet]:
|
||||
try:
|
||||
return session.get_node(node_id, WlanNode)
|
||||
except CoreError:
|
||||
try:
|
||||
return session.get_node(node_id, EmaneNet)
|
||||
except CoreError:
|
||||
context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane")
|
||||
|
|
|
@ -19,7 +19,6 @@ from core.api.grpc import (
|
|||
core_pb2_grpc,
|
||||
grpcutils,
|
||||
)
|
||||
from core.api.grpc.common_pb2 import MappedConfig
|
||||
from core.api.grpc.configservices_pb2 import (
|
||||
ConfigService,
|
||||
GetConfigServiceDefaultsRequest,
|
||||
|
@ -89,7 +88,6 @@ from core.api.grpc.services_pb2 import (
|
|||
ServiceAction,
|
||||
ServiceActionRequest,
|
||||
ServiceActionResponse,
|
||||
ServiceDefaults,
|
||||
SetNodeServiceFileRequest,
|
||||
SetNodeServiceFileResponse,
|
||||
SetNodeServiceRequest,
|
||||
|
@ -118,12 +116,13 @@ from core.emulator.enumerations import (
|
|||
from core.emulator.session import NT, Session
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
|
||||
from core.nodes.base import CoreNode, NodeBase
|
||||
from core.nodes.network import PtpNet, WlanNode
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
_ONE_DAY_IN_SECONDS: int = 60 * 60 * 24
|
||||
_INTERFACE_REGEX: Pattern = re.compile(r"veth(?P<node>[0-9a-fA-F]+)")
|
||||
_MAX_WORKERS = 1000
|
||||
|
||||
|
||||
class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
||||
|
@ -152,7 +151,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
|
||||
def listen(self, address: str) -> None:
|
||||
logging.info("CORE gRPC API listening on: %s", address)
|
||||
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
||||
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS))
|
||||
core_pb2_grpc.add_CoreApiServicer_to_server(self, self.server)
|
||||
self.server.add_insecure_port(address)
|
||||
self.server.start()
|
||||
|
@ -558,7 +557,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get session: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
|
||||
links = []
|
||||
nodes = []
|
||||
for _id in session.nodes:
|
||||
|
@ -568,9 +566,39 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
nodes.append(node_proto)
|
||||
node_links = get_links(node)
|
||||
links.extend(node_links)
|
||||
|
||||
default_services = grpcutils.get_default_services(session)
|
||||
x, y, z = session.location.refxyz
|
||||
lat, lon, alt = session.location.refgeo
|
||||
location = core_pb2.SessionLocation(
|
||||
x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale
|
||||
)
|
||||
hooks = grpcutils.get_hooks(session)
|
||||
emane_models = grpcutils.get_emane_models(session)
|
||||
emane_config = grpcutils.get_emane_config(session)
|
||||
emane_model_configs = grpcutils.get_emane_model_configs(session)
|
||||
wlan_configs = grpcutils.get_wlan_configs(session)
|
||||
mobility_configs = grpcutils.get_mobility_configs(session)
|
||||
service_configs = grpcutils.get_node_service_configs(session)
|
||||
config_service_configs = grpcutils.get_node_config_service_configs(session)
|
||||
session_proto = core_pb2.Session(
|
||||
state=session.state.value, nodes=nodes, links=links, dir=session.session_dir
|
||||
id=session.id,
|
||||
state=session.state.value,
|
||||
nodes=nodes,
|
||||
links=links,
|
||||
dir=session.session_dir,
|
||||
user=session.user,
|
||||
default_services=default_services,
|
||||
location=location,
|
||||
hooks=hooks,
|
||||
emane_models=emane_models,
|
||||
emane_config=emane_config,
|
||||
emane_model_configs=emane_model_configs,
|
||||
wlan_configs=wlan_configs,
|
||||
service_configs=service_configs,
|
||||
config_service_configs=config_service_configs,
|
||||
mobility_configs=mobility_configs,
|
||||
metadata=session.metadata,
|
||||
file=session.file_name,
|
||||
)
|
||||
return core_pb2.GetSessionResponse(session=session_proto)
|
||||
|
||||
|
@ -652,9 +680,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
key = key.split(".")
|
||||
node_id = _INTERFACE_REGEX.search(key[0]).group("node")
|
||||
node_id = int(node_id, base=16)
|
||||
iface_id = int(key[1], base=16)
|
||||
session_id = int(key[2], base=16)
|
||||
if session.id != session_id:
|
||||
iface_id = int(key[1])
|
||||
session_id = key[2]
|
||||
if session.short_session_id() != session_id:
|
||||
continue
|
||||
iface_throughput = throughputs_event.iface_throughputs.add()
|
||||
iface_throughput.node_id = node_id
|
||||
|
@ -664,8 +692,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
try:
|
||||
key = key.split(".")
|
||||
node_id = int(key[1], base=16)
|
||||
session_id = int(key[2], base=16)
|
||||
if session.id != session_id:
|
||||
session_id = key[2]
|
||||
if session.short_session_id() != session_id:
|
||||
continue
|
||||
bridge_throughput = (
|
||||
throughputs_event.bridge_throughputs.add()
|
||||
|
@ -1012,12 +1040,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get hooks: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
hooks = []
|
||||
for state in session.hooks:
|
||||
state_hooks = session.hooks[state]
|
||||
for file_name, file_data in state_hooks:
|
||||
hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data)
|
||||
hooks.append(hook)
|
||||
hooks = grpcutils.get_hooks(session)
|
||||
return core_pb2.GetHooksResponse(hooks=hooks)
|
||||
|
||||
def AddHook(
|
||||
|
@ -1050,19 +1073,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get mobility configs: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
response = GetMobilityConfigsResponse()
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != Ns2ScriptedMobility.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, Ns2ScriptedMobility)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
response.configs[node_id].CopyFrom(mapped_config)
|
||||
return response
|
||||
configs = grpcutils.get_mobility_configs(session)
|
||||
return GetMobilityConfigsResponse(configs=configs)
|
||||
|
||||
def GetMobilityConfig(
|
||||
self, request: GetMobilityConfigRequest, context: ServicerContext
|
||||
|
@ -1115,7 +1127,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("mobility action: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
node = self.get_node(session, request.node_id, context, WlanNode)
|
||||
node = grpcutils.get_mobility_node(session, request.node_id, context)
|
||||
if not node.mobility:
|
||||
context.abort(
|
||||
grpc.StatusCode.NOT_FOUND, f"node({node.name}) does not have mobility"
|
||||
)
|
||||
result = True
|
||||
if request.action == MobilityAction.START:
|
||||
node.mobility.start()
|
||||
|
@ -1157,12 +1173,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get service defaults: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
all_service_defaults = []
|
||||
for node_type in session.services.default_services:
|
||||
services = session.services.default_services[node_type]
|
||||
service_defaults = ServiceDefaults(node_type=node_type, services=services)
|
||||
all_service_defaults.append(service_defaults)
|
||||
return GetServiceDefaultsResponse(defaults=all_service_defaults)
|
||||
defaults = grpcutils.get_default_services(session)
|
||||
return GetServiceDefaultsResponse(defaults=defaults)
|
||||
|
||||
def SetServiceDefaults(
|
||||
self, request: SetServiceDefaultsRequest, context: ServicerContext
|
||||
|
@ -1196,18 +1208,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get node service configs: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
configs = []
|
||||
for node_id, service_configs in session.services.custom_services.items():
|
||||
for name in service_configs:
|
||||
service = session.services.get_service(node_id, name)
|
||||
service_proto = grpcutils.get_service_configuration(service)
|
||||
config = GetNodeServiceConfigsResponse.ServiceConfig(
|
||||
node_id=node_id,
|
||||
service=name,
|
||||
data=service_proto,
|
||||
files=service.config_data,
|
||||
)
|
||||
configs.append(config)
|
||||
configs = grpcutils.get_node_service_configs(session)
|
||||
return GetNodeServiceConfigsResponse(configs=configs)
|
||||
|
||||
def GetNodeService(
|
||||
|
@ -1337,19 +1338,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get wlan configs: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
response = GetWlanConfigsResponse()
|
||||
for node_id in session.mobility.node_configurations:
|
||||
model_config = session.mobility.node_configurations[node_id]
|
||||
if node_id == -1:
|
||||
continue
|
||||
for model_name in model_config:
|
||||
if model_name != BasicRangeModel.name:
|
||||
continue
|
||||
current_config = session.mobility.get_model_config(node_id, model_name)
|
||||
config = get_config_options(current_config, BasicRangeModel)
|
||||
mapped_config = MappedConfig(config=config)
|
||||
response.configs[node_id].CopyFrom(mapped_config)
|
||||
return response
|
||||
configs = grpcutils.get_wlan_configs(session)
|
||||
return GetWlanConfigsResponse(configs=configs)
|
||||
|
||||
def GetWlanConfig(
|
||||
self, request: GetWlanConfigRequest, context: ServicerContext
|
||||
|
@ -1401,8 +1391,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get emane config: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
current_config = session.emane.get_configs()
|
||||
config = get_config_options(current_config, session.emane.emane_config)
|
||||
config = grpcutils.get_emane_config(session)
|
||||
return GetEmaneConfigResponse(config=config)
|
||||
|
||||
def SetEmaneConfig(
|
||||
|
@ -1433,11 +1422,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get emane models: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
models = []
|
||||
for model in session.emane.models.keys():
|
||||
if len(model.split("_")) != 2:
|
||||
continue
|
||||
models.append(model)
|
||||
models = grpcutils.get_emane_models(session)
|
||||
return GetEmaneModelsResponse(models=models)
|
||||
|
||||
def GetEmaneModelConfig(
|
||||
|
@ -1491,22 +1476,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
"""
|
||||
logging.debug("get emane model configs: %s", request)
|
||||
session = self.get_session(request.session_id, context)
|
||||
|
||||
configs = []
|
||||
for _id in session.emane.node_configurations:
|
||||
if _id == -1:
|
||||
continue
|
||||
|
||||
model_configs = session.emane.node_configurations[_id]
|
||||
for model_name in model_configs:
|
||||
model = session.emane.models[model_name]
|
||||
current_config = session.emane.get_model_config(_id, model_name)
|
||||
config = get_config_options(current_config, model)
|
||||
node_id, iface_id = grpcutils.parse_emane_model_id(_id)
|
||||
model_config = GetEmaneModelConfigsResponse.ModelConfig(
|
||||
node_id=node_id, model=model_name, iface_id=iface_id, config=config
|
||||
)
|
||||
configs.append(model_config)
|
||||
configs = grpcutils.get_emane_model_configs(session)
|
||||
return GetEmaneModelConfigsResponse(configs=configs)
|
||||
|
||||
def SaveXml(
|
||||
|
@ -1713,21 +1683,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
:return: get node config service configs response
|
||||
"""
|
||||
session = self.get_session(request.session_id, context)
|
||||
configs = []
|
||||
for node in session.nodes.values():
|
||||
if not isinstance(node, CoreNodeBase):
|
||||
continue
|
||||
|
||||
for name, service in node.config_services.items():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
configs = grpcutils.get_node_config_service_configs(session)
|
||||
return GetNodeConfigServiceConfigsResponse(configs=configs)
|
||||
|
||||
def GetNodeConfigServices(
|
||||
|
|
|
@ -520,8 +520,8 @@ class CoreLinkTlv(CoreTlv):
|
|||
LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
|
||||
LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
|
||||
LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
|
||||
LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
|
||||
LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
|
||||
LinkTlvs.IFACE1_NAME.value: CoreTlvDataString,
|
||||
LinkTlvs.IFACE2_NAME.value: CoreTlvDataString,
|
||||
LinkTlvs.OPAQUE.value: CoreTlvDataString,
|
||||
}
|
||||
|
||||
|
|
|
@ -759,7 +759,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
|
||||
iface1_data = InterfaceData(
|
||||
id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value),
|
||||
name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
|
||||
name=message.get_tlv(LinkTlvs.IFACE1_NAME.value),
|
||||
mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
|
||||
ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
|
||||
ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
|
||||
|
@ -768,7 +768,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
)
|
||||
iface2_data = InterfaceData(
|
||||
id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
|
||||
name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
|
||||
name=message.get_tlv(LinkTlvs.IFACE2_NAME.value),
|
||||
mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
|
||||
ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
|
||||
ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
|
||||
|
@ -1903,12 +1903,12 @@ class CoreHandler(socketserver.BaseRequestHandler):
|
|||
# TODO: send location info
|
||||
|
||||
# send hook scripts
|
||||
for state in sorted(self.session.hooks.keys()):
|
||||
for state in sorted(self.session.hooks):
|
||||
for file_name, config_data in self.session.hooks[state]:
|
||||
file_data = FileData(
|
||||
message_type=MessageFlags.ADD,
|
||||
name=str(file_name),
|
||||
type=f"hook:{state}",
|
||||
type=f"hook:{state.value}",
|
||||
data=str(config_data),
|
||||
)
|
||||
self.session.broadcast_file(file_data)
|
||||
|
|
|
@ -84,8 +84,8 @@ class LinkTlvs(Enum):
|
|||
IFACE2_MAC = 0x39
|
||||
IFACE2_IP6 = 0x40
|
||||
IFACE2_IP6_MASK = 0x41
|
||||
INTERFACE1_NAME = 0x42
|
||||
INTERFACE2_NAME = 0x43
|
||||
IFACE1_NAME = 0x42
|
||||
IFACE2_NAME = 0x43
|
||||
OPAQUE = 0x50
|
||||
|
||||
|
||||
|
|
|
@ -130,8 +130,8 @@ class EmaneModel(WirelessModel):
|
|||
:return: nothing
|
||||
"""
|
||||
try:
|
||||
wlan = self.session.get_node(self.id, EmaneNet)
|
||||
wlan.setnempositions(moved_ifaces)
|
||||
emane_net = self.session.get_node(self.id, EmaneNet)
|
||||
emane_net.setnempositions(moved_ifaces)
|
||||
except CoreError:
|
||||
logging.exception("error during update")
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import os
|
|||
import pwd
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
@ -119,7 +120,7 @@ class Session:
|
|||
# states and hooks handlers
|
||||
self.state: EventTypes = EventTypes.DEFINITION_STATE
|
||||
self.state_time: float = time.monotonic()
|
||||
self.hooks: Dict[EventTypes, Tuple[str, str]] = {}
|
||||
self.hooks: Dict[EventTypes, List[Tuple[str, str]]] = {}
|
||||
self.state_hooks: Dict[EventTypes, List[Callable[[EventTypes], None]]] = {}
|
||||
self.add_state_hook(
|
||||
state=EventTypes.RUNTIME_STATE, hook=self.runtime_state_hook
|
||||
|
@ -621,7 +622,8 @@ class Session:
|
|||
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Determine if this session is considered to be active. (Runtime or Data collect states)
|
||||
Determine if this session is considered to be active.
|
||||
(Runtime or Data collect states)
|
||||
|
||||
:return: True if active, False otherwise
|
||||
"""
|
||||
|
@ -991,6 +993,7 @@ class Session:
|
|||
:return: environment variables
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["CORE_PYTHON"] = sys.executable
|
||||
env["SESSION"] = str(self.id)
|
||||
env["SESSION_SHORT"] = self.short_session_id()
|
||||
env["SESSION_DIR"] = self.session_dir
|
||||
|
@ -1098,7 +1101,8 @@ class Session:
|
|||
|
||||
def delete_node(self, _id: int) -> bool:
|
||||
"""
|
||||
Delete a node from the session and check if session should shutdown, if no nodes are left.
|
||||
Delete a node from the session and check if session should shutdown, if no nodes
|
||||
are left.
|
||||
|
||||
:param _id: id of node to delete
|
||||
:return: True if node deleted, False otherwise
|
||||
|
|
|
@ -111,13 +111,13 @@ class Application(ttk.Frame):
|
|||
self.master.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.columnconfigure(1, weight=1)
|
||||
self.grid(sticky="nsew")
|
||||
self.grid(sticky=tk.NSEW)
|
||||
self.toolbar = Toolbar(self)
|
||||
self.toolbar.grid(sticky="ns")
|
||||
self.toolbar.grid(sticky=tk.NS)
|
||||
self.right_frame = ttk.Frame(self)
|
||||
self.right_frame.columnconfigure(0, weight=1)
|
||||
self.right_frame.rowconfigure(0, weight=1)
|
||||
self.right_frame.grid(row=0, column=1, sticky="nsew")
|
||||
self.right_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
self.draw_canvas()
|
||||
self.draw_infobar()
|
||||
self.draw_status()
|
||||
|
@ -139,21 +139,21 @@ class Application(ttk.Frame):
|
|||
canvas_frame = ttk.Frame(self.right_frame)
|
||||
canvas_frame.rowconfigure(0, weight=1)
|
||||
canvas_frame.columnconfigure(0, weight=1)
|
||||
canvas_frame.grid(row=0, column=0, sticky="nsew", pady=1)
|
||||
canvas_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=1)
|
||||
self.canvas = CanvasGraph(canvas_frame, self, self.core)
|
||||
self.canvas.grid(sticky="nsew")
|
||||
self.canvas.grid(sticky=tk.NSEW)
|
||||
scroll_y = ttk.Scrollbar(canvas_frame, command=self.canvas.yview)
|
||||
scroll_y.grid(row=0, column=1, sticky="ns")
|
||||
scroll_y.grid(row=0, column=1, sticky=tk.NS)
|
||||
scroll_x = ttk.Scrollbar(
|
||||
canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.grid(row=1, column=0, sticky="ew")
|
||||
scroll_x.grid(row=1, column=0, sticky=tk.EW)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
def draw_status(self) -> None:
|
||||
self.statusbar = StatusBar(self.right_frame, self)
|
||||
self.statusbar.grid(sticky="ew", columnspan=2)
|
||||
self.statusbar.grid(sticky=tk.EW, columnspan=2)
|
||||
|
||||
def display_info(self, frame_class: Type[InfoFrameBase], **kwargs: Any) -> None:
|
||||
if not self.show_infobar.get():
|
||||
|
@ -161,7 +161,7 @@ class Application(ttk.Frame):
|
|||
self.clear_info()
|
||||
self.info_frame = frame_class(self.infobar, **kwargs)
|
||||
self.info_frame.draw()
|
||||
self.info_frame.grid(sticky="nsew")
|
||||
self.info_frame.grid(sticky=tk.NSEW)
|
||||
|
||||
def clear_info(self) -> None:
|
||||
if self.info_frame:
|
||||
|
@ -174,7 +174,7 @@ class Application(ttk.Frame):
|
|||
|
||||
def show_info(self) -> None:
|
||||
self.default_info()
|
||||
self.infobar.grid(row=0, column=1, sticky="nsew")
|
||||
self.infobar.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
def hide_info(self) -> None:
|
||||
self.infobar.grid_forget()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -46,9 +46,9 @@ class AboutDialog(Dialog):
|
|||
codetext = CodeText(self.top)
|
||||
codetext.text.insert("1.0", LICENSE)
|
||||
codetext.text.config(state=tk.DISABLED)
|
||||
codetext.grid(sticky="nsew")
|
||||
codetext.grid(sticky=tk.NSEW)
|
||||
|
||||
label = ttk.Label(
|
||||
self.top, text="Icons from https://icons8.com", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(sticky="ew")
|
||||
label.grid(sticky=tk.EW)
|
||||
|
|
|
@ -5,10 +5,10 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText
|
||||
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -30,13 +30,13 @@ class AlertsDialog(Dialog):
|
|||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame,
|
||||
columns=("time", "level", "session_id", "node", "source"),
|
||||
show="headings",
|
||||
)
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
self.tree.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.tree.column("time", stretch=tk.YES)
|
||||
self.tree.heading("time", text="Time")
|
||||
self.tree.column("level", stretch=tk.YES, width=100)
|
||||
|
@ -49,9 +49,8 @@ class AlertsDialog(Dialog):
|
|||
self.tree.heading("source", text="Source")
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
for alarm in self.app.statusbar.core_alarms:
|
||||
exception = alarm.exception_event
|
||||
level_name = ExceptionLevel.Enum.Name(exception.level)
|
||||
for exception in self.app.statusbar.core_alarms:
|
||||
level_name = exception.level.name
|
||||
node_id = exception.node_id if exception.node_id else ""
|
||||
insert_id = self.tree.insert(
|
||||
"",
|
||||
|
@ -60,43 +59,43 @@ class AlertsDialog(Dialog):
|
|||
values=(
|
||||
exception.date,
|
||||
level_name,
|
||||
alarm.session_id,
|
||||
exception.session_id,
|
||||
node_id,
|
||||
exception.source,
|
||||
),
|
||||
tags=(level_name,),
|
||||
)
|
||||
self.alarm_map[insert_id] = alarm
|
||||
self.alarm_map[insert_id] = exception
|
||||
|
||||
error_name = ExceptionLevel.Enum.Name(ExceptionLevel.ERROR)
|
||||
error_name = ExceptionLevel.ERROR.name
|
||||
self.tree.tag_configure(error_name, background="#ff6666")
|
||||
fatal_name = ExceptionLevel.Enum.Name(ExceptionLevel.FATAL)
|
||||
fatal_name = ExceptionLevel.FATAL.name
|
||||
self.tree.tag_configure(fatal_name, background="#d9d9d9")
|
||||
warning_name = ExceptionLevel.Enum.Name(ExceptionLevel.WARNING)
|
||||
warning_name = ExceptionLevel.WARNING.name
|
||||
self.tree.tag_configure(warning_name, background="#ffff99")
|
||||
notice_name = ExceptionLevel.Enum.Name(ExceptionLevel.NOTICE)
|
||||
notice_name = ExceptionLevel.NOTICE.name
|
||||
self.tree.tag_configure(notice_name, background="#85e085")
|
||||
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
xscrollbar.grid(row=1, sticky=tk.EW)
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
self.codetext = CodeText(self.top)
|
||||
self.codetext.text.config(state=tk.DISABLED, height=11)
|
||||
self.codetext.grid(sticky="nsew", pady=PADY)
|
||||
self.codetext.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Reset", command=self.reset_alerts)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Close", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def reset_alerts(self) -> None:
|
||||
self.codetext.text.config(state=tk.NORMAL)
|
||||
|
@ -108,8 +107,8 @@ class AlertsDialog(Dialog):
|
|||
|
||||
def click_select(self, event: tk.Event) -> None:
|
||||
current = self.tree.selection()[0]
|
||||
alarm = self.alarm_map[current]
|
||||
exception = self.alarm_map[current]
|
||||
self.codetext.text.config(state=tk.NORMAL)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
self.codetext.text.insert(1.0, alarm.exception_event.text)
|
||||
self.codetext.text.insert(1.0, exception.text)
|
||||
self.codetext.text.config(state=tk.DISABLED)
|
||||
|
|
|
@ -23,11 +23,11 @@ class SizeAndScaleDialog(Dialog):
|
|||
"""
|
||||
super().__init__(app, "Canvas Size and Scale")
|
||||
self.canvas: CanvasGraph = self.app.canvas
|
||||
self.section_font: font.Font = font.Font(weight="bold")
|
||||
self.section_font: font.Font = font.Font(weight=font.BOLD)
|
||||
width, height = self.canvas.current_dimensions
|
||||
self.pixel_width: tk.IntVar = tk.IntVar(value=width)
|
||||
self.pixel_height: tk.IntVar = tk.IntVar(value=height)
|
||||
location = self.app.core.location
|
||||
location = self.app.core.session.location
|
||||
self.x: tk.DoubleVar = tk.DoubleVar(value=location.x)
|
||||
self.y: tk.DoubleVar = tk.DoubleVar(value=location.y)
|
||||
self.lat: tk.DoubleVar = tk.DoubleVar(value=location.lat)
|
||||
|
@ -54,68 +54,68 @@ class SizeAndScaleDialog(Dialog):
|
|||
|
||||
def draw_size(self) -> None:
|
||||
label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# draw size row 1
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_width)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveIntEntry(frame, textvariable=self.pixel_height)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="Pixels")
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
label.grid(row=0, column=4, sticky=tk.W)
|
||||
|
||||
# draw size row 2
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, textvariable=self.meters_width, state=tk.DISABLED
|
||||
)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
label = ttk.Label(frame, text="x Height")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, textvariable=self.meters_height, state=tk.DISABLED
|
||||
)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=4, sticky="w")
|
||||
label.grid(row=0, column=4, sticky=tk.W)
|
||||
|
||||
def draw_scale(self) -> None:
|
||||
label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.scale)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
entry.bind("<KeyRelease>", self.size_scale_keyup)
|
||||
label = ttk.Label(frame, text="Meters")
|
||||
label.grid(row=0, column=2, sticky="w")
|
||||
label.grid(row=0, column=2, sticky=tk.W)
|
||||
|
||||
def draw_reference_point(self) -> None:
|
||||
label_frame = ttk.Labelframe(
|
||||
self.top, text="Reference Point", padding=FRAME_PAD
|
||||
)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
label = ttk.Label(
|
||||
|
@ -124,61 +124,61 @@ class SizeAndScaleDialog(Dialog):
|
|||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="X")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.x)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Y")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
entry = validation.PositiveFloatEntry(frame, textvariable=self.y)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
|
||||
label = ttk.Label(label_frame, text="Translates To")
|
||||
label.grid()
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.columnconfigure(5, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Lat")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.lat)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Lon")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.lon)
|
||||
entry.grid(row=0, column=3, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=3, sticky=tk.EW, padx=PADX)
|
||||
|
||||
label = ttk.Label(frame, text="Alt")
|
||||
label.grid(row=0, column=4, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=4, sticky=tk.W, padx=PADX)
|
||||
entry = validation.FloatEntry(frame, textvariable=self.alt)
|
||||
entry.grid(row=0, column=5, sticky="ew")
|
||||
entry.grid(row=0, column=5, sticky=tk.EW)
|
||||
|
||||
def draw_save_as_default(self) -> None:
|
||||
button = ttk.Checkbutton(
|
||||
self.top, text="Save as default?", variable=self.save_default
|
||||
)
|
||||
button.grid(sticky="w", pady=PADY)
|
||||
button.grid(sticky=tk.W, pady=PADY)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def size_scale_keyup(self, _event: tk.Event) -> None:
|
||||
scale = self.scale.get()
|
||||
|
@ -192,7 +192,7 @@ class SizeAndScaleDialog(Dialog):
|
|||
self.canvas.redraw_canvas((width, height))
|
||||
if self.canvas.wallpaper:
|
||||
self.canvas.redraw_wallpaper()
|
||||
location = self.app.core.location
|
||||
location = self.app.core.session.location
|
||||
location.x = self.x.get()
|
||||
location.y = self.y.get()
|
||||
location.lat = self.lat.get()
|
||||
|
|
|
@ -51,7 +51,7 @@ class CanvasWallpaperDialog(Dialog):
|
|||
|
||||
def draw_image_label(self) -> None:
|
||||
label = ttk.Label(self.top, text="Image filename: ")
|
||||
label.grid(sticky="ew")
|
||||
label.grid(sticky=tk.EW)
|
||||
if self.filename.get():
|
||||
self.draw_preview()
|
||||
|
||||
|
@ -60,17 +60,17 @@ class CanvasWallpaperDialog(Dialog):
|
|||
frame.columnconfigure(0, weight=2)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
entry = ttk.Entry(frame, textvariable=self.filename)
|
||||
entry.focus()
|
||||
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="...", command=self.click_open_image)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Clear", command=self.click_clear)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def draw_options(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -78,30 +78,30 @@ class CanvasWallpaperDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="upper-left", value=1, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
button.grid(row=0, column=0, sticky=tk.EW)
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="centered", value=2, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="scaled", value=3, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
self.options.append(button)
|
||||
|
||||
button = ttk.Radiobutton(
|
||||
frame, text="titled", value=4, variable=self.scale_option
|
||||
)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
self.options.append(button)
|
||||
|
||||
def draw_additional_options(self) -> None:
|
||||
|
@ -111,19 +111,19 @@ class CanvasWallpaperDialog(Dialog):
|
|||
variable=self.adjust_to_dim,
|
||||
command=self.click_adjust_canvas,
|
||||
)
|
||||
checkbutton.grid(sticky="ew", padx=PADX)
|
||||
checkbutton.grid(sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_open_image(self) -> None:
|
||||
filename = image_chooser(self, BACKGROUNDS_PATH)
|
||||
|
|
|
@ -48,13 +48,13 @@ class ColorPickerDialog(Dialog):
|
|||
|
||||
# rgb frames
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=0, column=0, sticky="ew", pady=PADY)
|
||||
frame.grid(row=0, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="R")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.red_entry = validation.RgbEntry(frame, width=3, textvariable=self.red)
|
||||
self.red_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.red_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
@ -64,20 +64,20 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.red_scale,
|
||||
command=lambda x: self.scale_callback(self.red_scale, self.red),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.red_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5
|
||||
)
|
||||
self.red_label.grid(row=0, column=3, sticky="ew")
|
||||
self.red_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
|
||||
frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="G")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.green_entry = validation.RgbEntry(frame, width=3, textvariable=self.green)
|
||||
self.green_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.green_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
@ -87,20 +87,20 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.green_scale,
|
||||
command=lambda x: self.scale_callback(self.green_scale, self.green),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.green_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5
|
||||
)
|
||||
self.green_label.grid(row=0, column=3, sticky="ew")
|
||||
self.green_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(2, weight=3)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
label = ttk.Label(frame, text="B")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.blue_entry = validation.RgbEntry(frame, width=3, textvariable=self.blue)
|
||||
self.blue_entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.blue_entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
scale = ttk.Scale(
|
||||
frame,
|
||||
from_=0,
|
||||
|
@ -110,31 +110,31 @@ class ColorPickerDialog(Dialog):
|
|||
variable=self.blue_scale,
|
||||
command=lambda x: self.scale_callback(self.blue_scale, self.blue),
|
||||
)
|
||||
scale.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
scale.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
self.blue_label = ttk.Label(
|
||||
frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5
|
||||
)
|
||||
self.blue_label.grid(row=0, column=3, sticky="ew")
|
||||
self.blue_label.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
# hex code and color display
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(1, weight=1)
|
||||
self.hex_entry = validation.HexEntry(frame, textvariable=self.hex)
|
||||
self.hex_entry.grid(sticky="ew", pady=PADY)
|
||||
self.hex_entry.grid(sticky=tk.EW, pady=PADY)
|
||||
self.display = tk.Frame(frame, background=self.color, width=100, height=100)
|
||||
self.display.grid(sticky="nsew")
|
||||
frame.grid(row=3, column=0, sticky="nsew", pady=PADY)
|
||||
self.display.grid(sticky=tk.NSEW)
|
||||
frame.grid(row=3, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
# button frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=4, column=0, sticky="ew")
|
||||
frame.grid(row=4, column=0, sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="OK", command=self.button_ok)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def set_bindings(self) -> None:
|
||||
self.red_entry.bind("<FocusIn>", lambda x: self.current_focus("rgb"))
|
||||
|
|
|
@ -8,32 +8,29 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.services_pb2 import ServiceValidationMode
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
from core.gui.wrappers import (
|
||||
ConfigOption,
|
||||
ConfigServiceData,
|
||||
Node,
|
||||
ServiceValidationMode,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
||||
|
||||
class ConfigServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
service_name: str,
|
||||
canvas_node: "CanvasNode",
|
||||
node_id: int,
|
||||
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
|
||||
) -> None:
|
||||
title = f"{service_name} Config Service"
|
||||
super().__init__(app, title, master=master)
|
||||
self.core: "CoreClient" = app.core
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node_id: int = node_id
|
||||
self.node: Node = node
|
||||
self.service_name: str = service_name
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
|
@ -51,7 +48,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
self.validation_time: Optional[int] = None
|
||||
self.validation_period: tk.StringVar = tk.StringVar()
|
||||
self.modes: List[str] = []
|
||||
self.mode_configs: Dict[str, str] = {}
|
||||
self.mode_configs: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
self.notebook: Optional[ttk.Notebook] = None
|
||||
self.templates_combobox: Optional[ttk.Combobox] = None
|
||||
|
@ -92,25 +89,18 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
response = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = response.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
|
||||
self.modes = sorted(x.name for x in response.modes)
|
||||
self.mode_configs = {x.name: x.config for x in response.modes}
|
||||
|
||||
service_config = self.canvas_node.config_service_configs.get(
|
||||
self.service_name, {}
|
||||
)
|
||||
self.config = response.config
|
||||
self.config = ConfigOption.from_dict(response.config)
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
custom_config = service_config.get("config")
|
||||
if custom_config:
|
||||
for key, value in custom_config.items():
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if service_config:
|
||||
for key, value in service_config.config.items():
|
||||
self.config[key].value = value
|
||||
logging.info("default config: %s", self.default_config)
|
||||
|
||||
custom_templates = service_config.get("templates", {})
|
||||
for file, data in custom_templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
logging.info("default config: %s", self.default_config)
|
||||
for file, data in service_config.templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Config Service Error", e)
|
||||
self.has_error = True
|
||||
|
@ -121,7 +111,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_tab_files()
|
||||
if self.config:
|
||||
self.draw_tab_config()
|
||||
|
@ -131,7 +121,7 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_files(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
|
@ -141,29 +131,29 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
label.grid(pady=PADY)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state="readonly"
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
self.template_text.grid(sticky="nsew")
|
||||
self.template_text.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
|
@ -175,13 +165,13 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_config(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration")
|
||||
|
||||
if self.modes:
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Modes")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
|
@ -189,17 +179,17 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
frame, values=self.modes, state="readonly"
|
||||
)
|
||||
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
logging.info("config service config: %s", self.config)
|
||||
self.config_frame = ConfigFrame(tab, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||
|
||||
def draw_tab_startstop(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
|
@ -225,12 +215,12 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
|
@ -240,23 +230,23 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_validation(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="ew")
|
||||
tab.grid(sticky=tk.EW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Validation", sticky="nsew")
|
||||
self.notebook.add(tab, text="Validation", sticky=tk.NSEW)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
if self.validation_mode == ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == ServiceValidationMode.NON_BLOCKING:
|
||||
|
@ -268,66 +258,64 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=2, column=0, sticky=tk.W, padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=self.validation_period
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom():
|
||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||
self.node.config_service_configs.pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
service_config = self.canvas_node.config_service_configs.setdefault(
|
||||
self.service_name, {}
|
||||
)
|
||||
service_config = self.node.config_service_configs.get(self.service_name)
|
||||
if not service_config:
|
||||
service_config = ConfigServiceData()
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
service_config["config"] = {x.name: x.value for x in self.config.values()}
|
||||
templates_config = service_config.setdefault("templates", {})
|
||||
service_config.config = {x.name: x.value for x in self.config.values()}
|
||||
for file in self.modified_files:
|
||||
templates_config[file] = self.temp_service_files[file]
|
||||
service_config.templates[file] = self.temp_service_files[file]
|
||||
all_current = current_listbox.get(0, tk.END)
|
||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||
self.destroy()
|
||||
|
@ -361,9 +349,9 @@ class ConfigServiceConfigDialog(Dialog):
|
|||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self) -> None:
|
||||
self.canvas_node.config_service_configs.pop(self.service_name, None)
|
||||
self.node.config_service_configs.pop(self.service_name, None)
|
||||
logging.info(
|
||||
"cleared config service config: %s", self.canvas_node.config_service_configs
|
||||
"cleared config service config: %s", self.node.config_service_configs
|
||||
)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
filename = self.templates_combobox.get()
|
||||
|
|
|
@ -38,41 +38,40 @@ class CopyServiceConfigDialog(Dialog):
|
|||
label = ttk.Label(
|
||||
self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.listbox = listbox_scroll.listbox
|
||||
for canvas_node in self.app.canvas.nodes.values():
|
||||
file_configs = canvas_node.service_file_configs.get(self.service)
|
||||
for node in self.app.core.session.nodes.values():
|
||||
file_configs = node.service_file_configs.get(self.service)
|
||||
if not file_configs:
|
||||
continue
|
||||
data = file_configs.get(self.file_name)
|
||||
if not data:
|
||||
continue
|
||||
name = canvas_node.core_node.name
|
||||
self.nodes[name] = canvas_node.id
|
||||
self.listbox.insert(tk.END, name)
|
||||
self.nodes[node.name] = node.id
|
||||
self.listbox.insert(tk.END, node.name)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Copy", command=self.click_copy)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="View", command=self.click_view)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def click_copy(self) -> None:
|
||||
selection = self.listbox.curselection()
|
||||
if not selection:
|
||||
return
|
||||
name = self.listbox.get(selection)
|
||||
canvas_node_id = self.nodes[name]
|
||||
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||
node_id = self.nodes[name]
|
||||
node = self.app.core.session.nodes[node_id]
|
||||
data = node.service_file_configs[self.service][self.file_name]
|
||||
self.dialog.temp_service_files[self.file_name] = data
|
||||
self.dialog.modified_files.add(self.file_name)
|
||||
self.dialog.service_file_data.text.delete(1.0, tk.END)
|
||||
|
@ -84,9 +83,9 @@ class CopyServiceConfigDialog(Dialog):
|
|||
if not selection:
|
||||
return
|
||||
name = self.listbox.get(selection)
|
||||
canvas_node_id = self.nodes[name]
|
||||
canvas_node = self.app.canvas.nodes[canvas_node_id]
|
||||
data = canvas_node.service_file_configs[self.service][self.file_name]
|
||||
node_id = self.nodes[name]
|
||||
node = self.app.core.session.nodes[node_id]
|
||||
data = node.service_file_configs[self.service][self.file_name]
|
||||
dialog = ViewConfigDialog(
|
||||
self.app, self, name, self.service, self.file_name, data
|
||||
)
|
||||
|
@ -113,8 +112,8 @@ class ViewConfigDialog(Dialog):
|
|||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
self.service_data = CodeText(self.top)
|
||||
self.service_data.grid(sticky="nsew", pady=PADY)
|
||||
self.service_data.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.service_data.text.insert(tk.END, self.data)
|
||||
self.service_data.text.config(state=tk.DISABLED)
|
||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
|
|
|
@ -34,47 +34,47 @@ class ServicesSelectDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.LabelFrame(self.top)
|
||||
frame.grid(stick="nsew", pady=PADY)
|
||||
frame.grid(stick=tk.NSEW, pady=PADY)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
self.groups.grid(sticky=tk.NSEW)
|
||||
for group in sorted(self.app.core.services):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
self.services.grid(sticky=tk.NSEW)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
self.current.grid(sticky=tk.NSEW)
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
frame.grid(stick=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.destroy)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
@ -127,58 +127,58 @@ class CustomNodesDialog(Dialog):
|
|||
|
||||
def draw_node_config(self) -> None:
|
||||
frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.nodes_list = ListboxScroll(frame)
|
||||
self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX)
|
||||
self.nodes_list.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX)
|
||||
self.nodes_list.listbox.bind("<<ListboxSelect>>", self.handle_node_select)
|
||||
for name in sorted(self.app.core.custom_nodes):
|
||||
self.nodes_list.listbox.insert(tk.END, name)
|
||||
|
||||
frame = ttk.Frame(frame)
|
||||
frame.grid(row=0, column=2, sticky="nsew")
|
||||
frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(sticky="ew", pady=PADY)
|
||||
entry.grid(sticky=tk.EW, pady=PADY)
|
||||
self.image_button = ttk.Button(
|
||||
frame, text="Icon", compound=tk.LEFT, command=self.click_icon
|
||||
)
|
||||
self.image_button.grid(sticky="ew", pady=PADY)
|
||||
self.image_button.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(frame, text="Services", command=self.click_services)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
|
||||
def draw_node_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.edit_button = ttk.Button(
|
||||
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
|
||||
)
|
||||
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def reset_values(self) -> None:
|
||||
self.name.set("")
|
||||
|
|
|
@ -30,7 +30,7 @@ class Dialog(tk.Toplevel):
|
|||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.top: ttk.Frame = ttk.Frame(self, padding=DIALOG_PAD)
|
||||
self.top.grid(sticky="nsew")
|
||||
self.top.grid(sticky=tk.NSEW)
|
||||
|
||||
def show(self) -> None:
|
||||
self.transient(self.master)
|
||||
|
@ -44,6 +44,6 @@ class Dialog(tk.Toplevel):
|
|||
|
||||
def draw_spacer(self, row: int = None) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=row, sticky="nsew")
|
||||
frame.grid(row=row, sticky=tk.NSEW)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
self.top.rowconfigure(frame.grid_info()["row"], weight=1)
|
||||
|
|
|
@ -8,16 +8,14 @@ from typing import TYPE_CHECKING, Dict, List, Optional
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
from core.gui.wrappers import ConfigOption, Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class GlobalEmaneDialog(Dialog):
|
||||
|
@ -30,24 +28,24 @@ class GlobalEmaneDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
session = self.app.core.session
|
||||
self.config_frame = ConfigFrame(
|
||||
self.top, self.app, self.app.core.emane_config, self.enabled
|
||||
self.top, self.app, session.emane_config, self.enabled
|
||||
)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
|
@ -59,24 +57,19 @@ class EmaneModelDialog(Dialog):
|
|||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
canvas_node: "CanvasNode",
|
||||
node: Node,
|
||||
model: str,
|
||||
iface_id: int = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
app, f"{canvas_node.core_node.name} {model} Configuration", master=master
|
||||
)
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
super().__init__(app, f"{node.name} {model} Configuration", master=master)
|
||||
self.node: Node = node
|
||||
self.model: str = f"emane_{model}"
|
||||
self.iface_id: int = iface_id
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.enabled: bool = not self.app.core.is_runtime()
|
||||
self.has_error: bool = False
|
||||
try:
|
||||
config = self.canvas_node.emane_model_configs.get(
|
||||
(self.model, self.iface_id)
|
||||
)
|
||||
config = self.node.emane_model_configs.get((self.model, self.iface_id))
|
||||
if not config:
|
||||
config = self.app.core.get_emane_model_config(
|
||||
self.node.id, self.model, self.iface_id
|
||||
|
@ -93,37 +86,35 @@ class EmaneModelDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_spacer()
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
key = (self.model, self.iface_id)
|
||||
self.canvas_node.emane_model_configs[key] = self.config
|
||||
self.node.emane_model_configs[key] = self.config
|
||||
self.destroy()
|
||||
|
||||
|
||||
class EmaneConfigDialog(Dialog):
|
||||
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||
super().__init__(app, f"{canvas_node.core_node.name} EMANE Configuration")
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
super().__init__(app, f"{node.name} EMANE Configuration")
|
||||
self.node: Node = node
|
||||
self.radiovar: tk.IntVar = tk.IntVar()
|
||||
self.radiovar.set(1)
|
||||
self.emane_models: List[str] = [
|
||||
x.split("_")[1] for x in self.app.core.emane_models
|
||||
x.split("_")[1] for x in self.app.core.session.emane_models
|
||||
]
|
||||
model = self.node.emane.split("_")[1]
|
||||
self.emane_model: tk.StringVar = tk.StringVar(value=model)
|
||||
|
@ -163,30 +154,30 @@ class EmaneConfigDialog(Dialog):
|
|||
),
|
||||
)
|
||||
button.image = image
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
button.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
def draw_emane_models(self) -> None:
|
||||
"""
|
||||
create a combobox that has all the known emane models
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Model")
|
||||
label.grid(row=0, column=0, sticky="w")
|
||||
label.grid(row=0, column=0, sticky=tk.W)
|
||||
|
||||
# create combo box and its binding
|
||||
state = "readonly" if self.enabled else tk.DISABLED
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.emane_model, values=self.emane_models, state=state
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky="ew")
|
||||
combobox.grid(row=0, column=1, sticky=tk.EW)
|
||||
combobox.bind("<<ComboboxSelected>>", self.emane_model_change)
|
||||
|
||||
def draw_emane_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
|
@ -199,7 +190,7 @@ class EmaneConfigDialog(Dialog):
|
|||
command=self.click_model_config,
|
||||
)
|
||||
self.emane_model_button.image = image
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.EDITNODE, 16)
|
||||
button = ttk.Button(
|
||||
|
@ -210,18 +201,18 @@ class EmaneConfigDialog(Dialog):
|
|||
command=self.click_emane_config,
|
||||
)
|
||||
button.image = image
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def draw_apply_and_cancel(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_emane_config(self) -> None:
|
||||
dialog = GlobalEmaneDialog(self, self.app)
|
||||
|
@ -232,7 +223,7 @@ class EmaneConfigDialog(Dialog):
|
|||
draw emane model configuration
|
||||
"""
|
||||
model_name = self.emane_model.get()
|
||||
dialog = EmaneModelDialog(self, self.app, self.canvas_node, model_name)
|
||||
dialog = EmaneModelDialog(self, self.app, self.node, model_name)
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import tkinter as tk
|
||||
import webbrowser
|
||||
from tkinter import ttk
|
||||
|
||||
|
@ -13,13 +14,13 @@ class EmaneInstallDialog(Dialog):
|
|||
def draw(self) -> None:
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
label = ttk.Label(self.top, text="EMANE needs to be installed!")
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(
|
||||
self.top, text="EMANE Documentation", command=self.click_doc
|
||||
)
|
||||
button.grid(sticky="ew", pady=PADY)
|
||||
button.grid(sticky=tk.EW, pady=PADY)
|
||||
button = ttk.Button(self.top, text="Close", command=self.destroy)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
|
||||
def click_doc(self) -> None:
|
||||
webbrowser.open_new("https://coreemu.github.io/core/emane.html")
|
||||
|
|
|
@ -25,13 +25,13 @@ class ExecutePythonDialog(Dialog):
|
|||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=i, column=0, sticky="nsew")
|
||||
frame.grid(row=i, column=0, sticky=tk.NSEW)
|
||||
i = i + 1
|
||||
var = tk.StringVar(value="")
|
||||
self.file_entry = ttk.Entry(frame, textvariable=var)
|
||||
self.file_entry.grid(row=0, column=0, sticky="ew")
|
||||
self.file_entry.grid(row=0, column=0, sticky=tk.EW)
|
||||
button = ttk.Button(frame, text="...", command=self.select_file)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
button = ttk.Checkbutton(
|
||||
|
@ -40,18 +40,18 @@ class ExecutePythonDialog(Dialog):
|
|||
variable=self.with_options,
|
||||
command=self.add_options,
|
||||
)
|
||||
button.grid(row=i, column=0, sticky="ew")
|
||||
button.grid(row=i, column=0, sticky=tk.EW)
|
||||
i = i + 1
|
||||
|
||||
label = ttk.Label(
|
||||
self.top, text="Any command-line options for running the Python script"
|
||||
)
|
||||
label.grid(row=i, column=0, sticky="ew")
|
||||
label.grid(row=i, column=0, sticky=tk.EW)
|
||||
i = i + 1
|
||||
self.option_entry = ttk.Entry(
|
||||
self.top, textvariable=self.options, state="disabled"
|
||||
)
|
||||
self.option_entry.grid(row=i, column=0, sticky="ew")
|
||||
self.option_entry.grid(row=i, column=0, sticky=tk.EW)
|
||||
i = i + 1
|
||||
|
||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||
|
@ -59,9 +59,9 @@ class ExecutePythonDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=i, column=0)
|
||||
button = ttk.Button(frame, text="Execute", command=self.script_execute)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
def add_options(self) -> None:
|
||||
if self.with_options.get():
|
||||
|
|
|
@ -25,25 +25,25 @@ class FindDialog(Dialog):
|
|||
|
||||
# Find node frame
|
||||
frame = ttk.Frame(self.top, padding=FRAME_PAD)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Find:")
|
||||
label.grid()
|
||||
entry = ttk.Entry(frame, textvariable=self.find_text)
|
||||
entry.grid(row=0, column=1, sticky="nsew")
|
||||
entry.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
# node list frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame,
|
||||
columns=("nodeid", "name", "location", "detail"),
|
||||
show="headings",
|
||||
selectmode=tk.BROWSE,
|
||||
)
|
||||
self.tree.grid(sticky="nsew", pady=PADY)
|
||||
self.tree.grid(sticky=tk.NSEW, pady=PADY)
|
||||
style = ttk.Style()
|
||||
heading_size = int(self.app.guiconfig.scale * 10)
|
||||
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
|
||||
|
@ -57,21 +57,21 @@ class FindDialog(Dialog):
|
|||
self.tree.heading("detail", text="Detail")
|
||||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
xscrollbar.grid(row=1, sticky=tk.EW)
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
# button frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Find", command=self.find_node)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.close_dialog)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def clear_treeview_items(self) -> None:
|
||||
"""
|
||||
|
@ -87,22 +87,19 @@ class FindDialog(Dialog):
|
|||
"""
|
||||
node_name = self.find_text.get().strip()
|
||||
self.clear_treeview_items()
|
||||
for node_id, node in sorted(
|
||||
self.app.core.canvas_nodes.items(), key=lambda x: x[0]
|
||||
):
|
||||
name = node.core_node.name
|
||||
for node in self.app.core.session.nodes.values():
|
||||
name = node.name
|
||||
if not node_name or node_name == name:
|
||||
pos_x = round(node.core_node.position.x, 1)
|
||||
pos_y = round(node.core_node.position.y, 1)
|
||||
pos_x = round(node.position.x, 1)
|
||||
pos_y = round(node.position.y, 1)
|
||||
# TODO: I am not sure what to insert for Detail column
|
||||
# leaving it blank for now
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
text=str(node_id),
|
||||
values=(node_id, name, f"<{pos_x}, {pos_y}>", ""),
|
||||
text=str(node.id),
|
||||
values=(node.id, name, f"<{pos_x}, {pos_y}>", ""),
|
||||
)
|
||||
|
||||
results = self.tree.get_children("")
|
||||
if results:
|
||||
self.tree.selection_set(results[0])
|
||||
|
@ -121,7 +118,7 @@ class FindDialog(Dialog):
|
|||
if item:
|
||||
self.app.canvas.delete("find")
|
||||
node_id = int(self.tree.item(item, "text"))
|
||||
canvas_node = self.app.core.canvas_nodes[node_id]
|
||||
canvas_node = self.app.core.get_canvas_node(node_id)
|
||||
|
||||
x0, y0, x1, y1 = self.app.canvas.bbox(canvas_node.id)
|
||||
dist = 5 * self.app.guiconfig.scale
|
||||
|
|
|
@ -2,10 +2,10 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
from core.gui.wrappers import Hook, SessionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -16,8 +16,9 @@ class HookDialog(Dialog):
|
|||
super().__init__(app, "Hook", master=master)
|
||||
self.name: tk.StringVar = tk.StringVar()
|
||||
self.codetext: Optional[CodeText] = None
|
||||
self.hook: core_pb2.Hook = core_pb2.Hook()
|
||||
self.hook: Optional[Hook] = None
|
||||
self.state: tk.StringVar = tk.StringVar()
|
||||
self.editing: bool = False
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -26,22 +27,22 @@ class HookDialog(Dialog):
|
|||
|
||||
# name and states
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=2)
|
||||
frame.columnconfigure(1, weight=7)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE")
|
||||
initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
values = tuple(x.name for x in SessionState)
|
||||
initial_state = SessionState.RUNTIME.name
|
||||
self.state.set(initial_state)
|
||||
self.name.set(f"{initial_state.lower()}_hook.sh")
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.state, values=values, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=2, sticky="ew")
|
||||
combobox.grid(row=0, column=2, sticky=tk.EW)
|
||||
combobox.bind("<<ComboboxSelected>>", self.state_change)
|
||||
|
||||
# data
|
||||
|
@ -54,36 +55,43 @@ class HookDialog(Dialog):
|
|||
"# specified state\n"
|
||||
),
|
||||
)
|
||||
self.codetext.grid(sticky="nsew", pady=PADY)
|
||||
self.codetext.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
# button row
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=lambda: self.save())
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def state_change(self, event: tk.Event) -> None:
|
||||
if self.editing:
|
||||
return
|
||||
state_name = self.state.get()
|
||||
self.name.set(f"{state_name.lower()}_hook.sh")
|
||||
|
||||
def set(self, hook: core_pb2.Hook) -> None:
|
||||
def set(self, hook: Hook) -> None:
|
||||
self.editing = True
|
||||
self.hook = hook
|
||||
self.name.set(hook.file)
|
||||
self.codetext.text.delete(1.0, tk.END)
|
||||
self.codetext.text.insert(tk.END, hook.data)
|
||||
state_name = core_pb2.SessionState.Enum.Name(hook.state)
|
||||
state_name = hook.state.name
|
||||
self.state.set(state_name)
|
||||
|
||||
def save(self) -> None:
|
||||
data = self.codetext.text.get("1.0", tk.END).strip()
|
||||
state_value = core_pb2.SessionState.Enum.Value(self.state.get())
|
||||
self.hook.file = self.name.get()
|
||||
self.hook.data = data
|
||||
self.hook.state = state_value
|
||||
state = SessionState[self.state.get()]
|
||||
file_name = self.name.get()
|
||||
if self.editing:
|
||||
self.hook.state = state
|
||||
self.hook.file = file_name
|
||||
self.hook.data = data
|
||||
else:
|
||||
self.hook = Hook(state=state, file=file_name, data=data)
|
||||
self.destroy()
|
||||
|
||||
|
||||
|
@ -94,6 +102,7 @@ class HooksDialog(Dialog):
|
|||
self.edit_button: Optional[ttk.Button] = None
|
||||
self.delete_button: Optional[ttk.Button] = None
|
||||
self.selected: Optional[str] = None
|
||||
self.selected_index: Optional[int] = None
|
||||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
|
@ -101,56 +110,65 @@ class HooksDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.listbox = listbox_scroll.listbox
|
||||
self.listbox.bind("<<ListboxSelect>>", self.select)
|
||||
for hook_file in self.app.core.hooks:
|
||||
self.listbox.insert(tk.END, hook_file)
|
||||
session = self.app.core.session
|
||||
for file in session.hooks:
|
||||
self.listbox.insert(tk.END, file)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
self.edit_button = ttk.Button(
|
||||
frame, text="Edit", state=tk.DISABLED, command=self.click_edit
|
||||
)
|
||||
self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.edit_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
self.delete_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def click_create(self) -> None:
|
||||
dialog = HookDialog(self, self.app)
|
||||
dialog.show()
|
||||
hook = dialog.hook
|
||||
if hook:
|
||||
self.app.core.hooks[hook.file] = hook
|
||||
self.app.core.session.hooks[hook.file] = hook
|
||||
self.listbox.insert(tk.END, hook.file)
|
||||
|
||||
def click_edit(self) -> None:
|
||||
hook = self.app.core.hooks[self.selected]
|
||||
session = self.app.core.session
|
||||
hook = session.hooks.pop(self.selected)
|
||||
dialog = HookDialog(self, self.app)
|
||||
dialog.set(hook)
|
||||
dialog.show()
|
||||
session.hooks[hook.file] = hook
|
||||
self.selected = hook.file
|
||||
self.listbox.delete(self.selected_index)
|
||||
self.listbox.insert(self.selected_index, hook.file)
|
||||
self.listbox.select_set(self.selected_index)
|
||||
|
||||
def click_delete(self) -> None:
|
||||
del self.app.core.hooks[self.selected]
|
||||
self.listbox.delete(tk.ANCHOR)
|
||||
session = self.app.core.session
|
||||
del session.hooks[self.selected]
|
||||
self.listbox.delete(self.selected_index)
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def select(self, event: tk.Event) -> None:
|
||||
if self.listbox.curselection():
|
||||
index = self.listbox.curselection()[0]
|
||||
self.selected = self.listbox.get(index)
|
||||
self.selected_index = self.listbox.curselection()[0]
|
||||
self.selected = self.listbox.get(self.selected_index)
|
||||
self.edit_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.selected = None
|
||||
self.selected_index = None
|
||||
self.edit_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
|
|
|
@ -34,7 +34,7 @@ class IpConfigDialog(Dialog):
|
|||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD)
|
||||
ip4_frame.columnconfigure(0, weight=1)
|
||||
|
@ -42,23 +42,23 @@ class IpConfigDialog(Dialog):
|
|||
ip4_frame.grid(row=0, column=0, stick="nsew")
|
||||
self.ip4_listbox = ListboxScroll(ip4_frame)
|
||||
self.ip4_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip4)
|
||||
self.ip4_listbox.grid(sticky="nsew", pady=PADY)
|
||||
self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY)
|
||||
for index, ip4 in enumerate(self.ip4s):
|
||||
self.ip4_listbox.listbox.insert(tk.END, ip4)
|
||||
if self.ip4 == ip4:
|
||||
self.ip4_listbox.listbox.select_set(index)
|
||||
self.ip4_entry = ttk.Entry(ip4_frame)
|
||||
self.ip4_entry.grid(sticky="ew", pady=PADY)
|
||||
self.ip4_entry.grid(sticky=tk.EW, pady=PADY)
|
||||
ip4_button_frame = ttk.Frame(ip4_frame)
|
||||
ip4_button_frame.columnconfigure(0, weight=1)
|
||||
ip4_button_frame.columnconfigure(1, weight=1)
|
||||
ip4_button_frame.grid(sticky="ew")
|
||||
ip4_button_frame.grid(sticky=tk.EW)
|
||||
ip4_add = ttk.Button(ip4_button_frame, text="Add", command=self.click_add_ip4)
|
||||
ip4_add.grid(row=0, column=0, sticky="ew")
|
||||
ip4_add.grid(row=0, column=0, sticky=tk.EW)
|
||||
ip4_del = ttk.Button(
|
||||
ip4_button_frame, text="Delete", command=self.click_del_ip4
|
||||
)
|
||||
ip4_del.grid(row=0, column=1, sticky="ew")
|
||||
ip4_del.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD)
|
||||
ip6_frame.columnconfigure(0, weight=1)
|
||||
|
@ -66,23 +66,23 @@ class IpConfigDialog(Dialog):
|
|||
ip6_frame.grid(row=0, column=1, stick="nsew")
|
||||
self.ip6_listbox = ListboxScroll(ip6_frame)
|
||||
self.ip6_listbox.listbox.bind("<<ListboxSelect>>", self.select_ip6)
|
||||
self.ip6_listbox.grid(sticky="nsew", pady=PADY)
|
||||
self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY)
|
||||
for index, ip6 in enumerate(self.ip6s):
|
||||
self.ip6_listbox.listbox.insert(tk.END, ip6)
|
||||
if self.ip6 == ip6:
|
||||
self.ip6_listbox.listbox.select_set(index)
|
||||
self.ip6_entry = ttk.Entry(ip6_frame)
|
||||
self.ip6_entry.grid(sticky="ew", pady=PADY)
|
||||
self.ip6_entry.grid(sticky=tk.EW, pady=PADY)
|
||||
ip6_button_frame = ttk.Frame(ip6_frame)
|
||||
ip6_button_frame.columnconfigure(0, weight=1)
|
||||
ip6_button_frame.columnconfigure(1, weight=1)
|
||||
ip6_button_frame.grid(sticky="ew")
|
||||
ip6_button_frame.grid(sticky=tk.EW)
|
||||
ip6_add = ttk.Button(ip6_button_frame, text="Add", command=self.click_add_ip6)
|
||||
ip6_add.grid(row=0, column=0, sticky="ew")
|
||||
ip6_add.grid(row=0, column=0, sticky=tk.EW)
|
||||
ip6_del = ttk.Button(
|
||||
ip6_button_frame, text="Delete", command=self.click_del_ip6
|
||||
)
|
||||
ip6_del.grid(row=0, column=1, sticky="ew")
|
||||
ip6_del.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
# draw buttons
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -90,9 +90,9 @@ class IpConfigDialog(Dialog):
|
|||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_add_ip4(self) -> None:
|
||||
ip4 = self.ip4_entry.get()
|
||||
|
|
|
@ -5,11 +5,11 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui import validation
|
||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.wrappers import Interface, Link, LinkOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -21,7 +21,7 @@ def get_int(var: tk.StringVar) -> Optional[int]:
|
|||
if value != "":
|
||||
return int(value)
|
||||
else:
|
||||
return None
|
||||
return 0
|
||||
|
||||
|
||||
def get_float(var: tk.StringVar) -> Optional[float]:
|
||||
|
@ -29,14 +29,15 @@ def get_float(var: tk.StringVar) -> Optional[float]:
|
|||
if value != "":
|
||||
return float(value)
|
||||
else:
|
||||
return None
|
||||
return 0.0
|
||||
|
||||
|
||||
class LinkConfigurationDialog(Dialog):
|
||||
def __init__(self, app: "Application", edge: "CanvasEdge") -> None:
|
||||
super().__init__(app, "Link Configuration")
|
||||
self.edge: "CanvasEdge" = edge
|
||||
self.is_symmetric: bool = edge.link.options.unidirectional is False
|
||||
|
||||
self.is_symmetric: bool = edge.link.is_symmetric()
|
||||
if self.is_symmetric:
|
||||
symmetry_var = tk.StringVar(value=">>")
|
||||
else:
|
||||
|
@ -72,14 +73,11 @@ class LinkConfigurationDialog(Dialog):
|
|||
label = ttk.Label(
|
||||
self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER
|
||||
)
|
||||
label.grid(row=0, column=0, sticky="ew", pady=PADY)
|
||||
label.grid(row=0, column=0, sticky=tk.EW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=1, column=0, sticky="ew", pady=PADY)
|
||||
button = ttk.Button(frame, text="Unlimited")
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
frame.grid(row=1, column=0, sticky=tk.EW, pady=PADY)
|
||||
if self.is_symmetric:
|
||||
button = ttk.Button(
|
||||
frame, textvariable=self.symmetry_var, command=self.change_symmetry
|
||||
|
@ -88,25 +86,25 @@ class LinkConfigurationDialog(Dialog):
|
|||
button = ttk.Button(
|
||||
frame, textvariable=self.symmetry_var, command=self.change_symmetry
|
||||
)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
|
||||
if self.is_symmetric:
|
||||
self.symmetric_frame = self.get_frame()
|
||||
self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
self.symmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
|
||||
else:
|
||||
self.asymmetric_frame = self.get_frame()
|
||||
self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY)
|
||||
self.asymmetric_frame.grid(row=2, column=0, sticky=tk.EW, pady=PADY)
|
||||
|
||||
self.draw_spacer(row=3)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.grid(row=4, column=0, sticky="ew")
|
||||
frame.grid(row=4, column=0, sticky=tk.EW)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def get_frame(self) -> ttk.Frame:
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -117,76 +115,76 @@ class LinkConfigurationDialog(Dialog):
|
|||
label_name = "Asymmetric Effects: Downstream / Upstream "
|
||||
row = 0
|
||||
label = ttk.Label(frame, text=label_name, anchor=tk.CENTER)
|
||||
label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY)
|
||||
label.grid(row=row, column=0, columnspan=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Bandwidth (bps)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.bandwidth
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.down_bandwidth
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Delay (us)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.delay
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.down_delay
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Jitter (us)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.jitter
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.down_jitter
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Loss (%)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, empty_enabled=False, textvariable=self.loss
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, empty_enabled=False, textvariable=self.down_loss
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Duplicate (%)")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.duplicate
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
if not self.is_symmetric:
|
||||
entry = validation.PositiveIntEntry(
|
||||
frame, empty_enabled=False, textvariable=self.down_duplicate
|
||||
)
|
||||
entry.grid(row=row, column=2, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Color")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
self.color_button = tk.Button(
|
||||
frame,
|
||||
textvariable=self.color,
|
||||
|
@ -196,15 +194,15 @@ class LinkConfigurationDialog(Dialog):
|
|||
highlightthickness=0,
|
||||
command=self.click_color,
|
||||
)
|
||||
self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
self.color_button.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
row = row + 1
|
||||
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=row, column=0, sticky="ew")
|
||||
label.grid(row=row, column=0, sticky=tk.EW)
|
||||
entry = validation.PositiveFloatEntry(
|
||||
frame, empty_enabled=False, textvariable=self.width
|
||||
)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
return frame
|
||||
|
||||
|
@ -223,32 +221,26 @@ class LinkConfigurationDialog(Dialog):
|
|||
delay = get_int(self.delay)
|
||||
duplicate = get_int(self.duplicate)
|
||||
loss = get_float(self.loss)
|
||||
options = core_pb2.LinkOptions(
|
||||
options = LinkOptions(
|
||||
bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss
|
||||
)
|
||||
link.options.CopyFrom(options)
|
||||
|
||||
iface1_id = None
|
||||
if link.HasField("iface1"):
|
||||
iface1_id = link.iface1.id
|
||||
iface2_id = None
|
||||
if link.HasField("iface2"):
|
||||
iface2_id = link.iface2.id
|
||||
|
||||
link.options = options
|
||||
iface1_id = link.iface1.id if link.iface1 else None
|
||||
iface2_id = link.iface2.id if link.iface2 else None
|
||||
if not self.is_symmetric:
|
||||
link.options.unidirectional = True
|
||||
asym_iface1 = None
|
||||
if iface1_id:
|
||||
asym_iface1 = core_pb2.Interface(id=iface1_id)
|
||||
if iface1_id is not None:
|
||||
asym_iface1 = Interface(id=iface1_id)
|
||||
asym_iface2 = None
|
||||
if iface2_id:
|
||||
asym_iface2 = core_pb2.Interface(id=iface2_id)
|
||||
if iface2_id is not None:
|
||||
asym_iface2 = Interface(id=iface2_id)
|
||||
down_bandwidth = get_int(self.down_bandwidth)
|
||||
down_jitter = get_int(self.down_jitter)
|
||||
down_delay = get_int(self.down_delay)
|
||||
down_duplicate = get_int(self.down_duplicate)
|
||||
down_loss = get_float(self.down_loss)
|
||||
options = core_pb2.LinkOptions(
|
||||
options = LinkOptions(
|
||||
bandwidth=down_bandwidth,
|
||||
jitter=down_jitter,
|
||||
delay=down_delay,
|
||||
|
@ -256,36 +248,21 @@ class LinkConfigurationDialog(Dialog):
|
|||
loss=down_loss,
|
||||
unidirectional=True,
|
||||
)
|
||||
self.edge.asymmetric_link = core_pb2.Link(
|
||||
self.edge.asymmetric_link = Link(
|
||||
node1_id=link.node2_id,
|
||||
node2_id=link.node1_id,
|
||||
iface1=asym_iface1,
|
||||
iface2=asym_iface2,
|
||||
iface1=asym_iface2,
|
||||
iface2=asym_iface1,
|
||||
options=options,
|
||||
)
|
||||
else:
|
||||
link.options.unidirectional = False
|
||||
self.edge.asymmetric_link = None
|
||||
|
||||
if self.app.core.is_runtime() and link.HasField("options"):
|
||||
session_id = self.app.core.session_id
|
||||
self.app.core.client.edit_link(
|
||||
session_id,
|
||||
link.node1_id,
|
||||
link.node2_id,
|
||||
link.options,
|
||||
iface1_id,
|
||||
iface2_id,
|
||||
)
|
||||
if self.app.core.is_runtime() and link.options:
|
||||
self.app.core.edit_link(link)
|
||||
if self.edge.asymmetric_link:
|
||||
self.app.core.client.edit_link(
|
||||
session_id,
|
||||
link.node2_id,
|
||||
link.node1_id,
|
||||
self.edge.asymmetric_link.options,
|
||||
iface1_id,
|
||||
iface2_id,
|
||||
)
|
||||
self.app.core.edit_link(self.edge.asymmetric_link)
|
||||
|
||||
# update edge label
|
||||
self.edge.draw_link_options()
|
||||
|
@ -316,7 +293,7 @@ class LinkConfigurationDialog(Dialog):
|
|||
color = self.app.canvas.itemcget(self.edge.id, "fill")
|
||||
self.color.set(color)
|
||||
link = self.edge.link
|
||||
if link.HasField("options"):
|
||||
if link.options:
|
||||
self.bandwidth.set(str(link.options.bandwidth))
|
||||
self.jitter.set(str(link.options.jitter))
|
||||
self.duplicate.set(str(link.options.dup))
|
||||
|
|
|
@ -28,7 +28,7 @@ class MacConfigDialog(Dialog):
|
|||
"provided value below and increment by value in order."
|
||||
)
|
||||
label = ttk.Label(self.top, text=text)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
# draw input
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -36,9 +36,9 @@ class MacConfigDialog(Dialog):
|
|||
frame.columnconfigure(1, weight=3)
|
||||
frame.grid(stick="ew", pady=PADY)
|
||||
label = ttk.Label(frame, text="Starting MAC")
|
||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.mac_var)
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
entry.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
# draw buttons
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -46,9 +46,9 @@ class MacConfigDialog(Dialog):
|
|||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_save(self) -> None:
|
||||
mac = self.mac_var.get()
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
"""
|
||||
mobility configuration
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
from core.gui.wrappers import ConfigOption, Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class MobilityConfigDialog(Dialog):
|
||||
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||
super().__init__(app, f"{canvas_node.core_node.name} Mobility Configuration")
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
super().__init__(app, f"{node.name} Mobility Configuration")
|
||||
self.node: Node = node
|
||||
self.config_frame: Optional[ConfigFrame] = None
|
||||
self.has_error: bool = False
|
||||
try:
|
||||
config = self.canvas_node.mobility_config
|
||||
config = self.node.mobility_config
|
||||
if not config:
|
||||
config = self.app.core.get_mobility_config(self.node.id)
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
|
@ -40,22 +38,22 @@ class MobilityConfigDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_apply_buttons()
|
||||
|
||||
def draw_apply_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
self.config_frame.parse_config()
|
||||
self.canvas_node.mobility_config = self.config
|
||||
self.node.mobility_config = self.config
|
||||
self.destroy()
|
||||
|
|
|
@ -1,40 +1,31 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.api.grpc.mobility_pb2 import MobilityAction
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.wrappers import MobilityAction, Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
ICON_SIZE: int = 16
|
||||
|
||||
|
||||
class MobilityPlayer:
|
||||
def __init__(
|
||||
self,
|
||||
app: "Application",
|
||||
canvas_node: "CanvasNode",
|
||||
config: Dict[str, ConfigOption],
|
||||
) -> None:
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
self.app: "Application" = app
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
self.node: Node = node
|
||||
self.dialog: Optional[MobilityPlayerDialog] = None
|
||||
self.state: Optional[MobilityAction] = None
|
||||
|
||||
def show(self) -> None:
|
||||
if self.dialog:
|
||||
self.dialog.destroy()
|
||||
self.dialog = MobilityPlayerDialog(self.app, self.canvas_node, self.config)
|
||||
self.dialog = MobilityPlayerDialog(self.app, self.node)
|
||||
self.dialog.protocol("WM_DELETE_WINDOW", self.close)
|
||||
if self.state == MobilityAction.START:
|
||||
self.set_play()
|
||||
|
@ -66,20 +57,11 @@ class MobilityPlayer:
|
|||
|
||||
|
||||
class MobilityPlayerDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
app: "Application",
|
||||
canvas_node: "CanvasNode",
|
||||
config: Dict[str, ConfigOption],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
app, f"{canvas_node.core_node.name} Mobility Player", modal=False
|
||||
)
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
super().__init__(app, f"{node.name} Mobility Player", modal=False)
|
||||
self.resizable(False, False)
|
||||
self.geometry("")
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node: Node = canvas_node.core_node
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
self.node: Node = node
|
||||
self.play_button: Optional[ttk.Button] = None
|
||||
self.pause_button: Optional[ttk.Button] = None
|
||||
self.stop_button: Optional[ttk.Button] = None
|
||||
|
@ -87,42 +69,43 @@ class MobilityPlayerDialog(Dialog):
|
|||
self.draw()
|
||||
|
||||
def draw(self) -> None:
|
||||
config = self.node.mobility_config
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
|
||||
file_name = self.config["file"].value
|
||||
file_name = config["file"].value
|
||||
label = ttk.Label(self.top, text=file_name)
|
||||
label.grid(sticky="ew", pady=PADY)
|
||||
label.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
self.progressbar = ttk.Progressbar(self.top, mode="indeterminate")
|
||||
self.progressbar.grid(sticky="ew", pady=PADY)
|
||||
self.progressbar.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.START, ICON_SIZE)
|
||||
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
||||
self.play_button.image = image
|
||||
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
self.play_button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.PAUSE, ICON_SIZE)
|
||||
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
||||
self.pause_button.image = image
|
||||
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.pause_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
image = self.app.get_icon(ImageEnum.STOP, ICON_SIZE)
|
||||
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
||||
self.stop_button.image = image
|
||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
self.stop_button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
|
||||
loop = tk.IntVar(value=int(self.config["loop"].value == "1"))
|
||||
loop = tk.IntVar(value=int(config["loop"].value == "1"))
|
||||
checkbutton = ttk.Checkbutton(
|
||||
frame, text="Loop?", variable=loop, state=tk.DISABLED
|
||||
)
|
||||
checkbutton.grid(row=0, column=3, padx=PADX)
|
||||
|
||||
rate = self.config["refresh_ms"].value
|
||||
rate = config["refresh_ms"].value
|
||||
label = ttk.Label(frame, text=f"rate {rate} ms")
|
||||
label.grid(row=0, column=4)
|
||||
|
||||
|
@ -148,30 +131,30 @@ class MobilityPlayerDialog(Dialog):
|
|||
|
||||
def click_play(self) -> None:
|
||||
self.set_play()
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.START
|
||||
session_id, self.node.id, MobilityAction.START.value
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Mobility Error", e)
|
||||
|
||||
def click_pause(self) -> None:
|
||||
self.set_pause()
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.PAUSE
|
||||
session_id, self.node.id, MobilityAction.PAUSE.value
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Mobility Error", e)
|
||||
|
||||
def click_stop(self) -> None:
|
||||
self.set_stop()
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
try:
|
||||
self.app.core.client.mobility_action(
|
||||
session_id, self.node.id, MobilityAction.STOP
|
||||
session_id, self.node.id, MobilityAction.STOP.value
|
||||
)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Mobility Error", e)
|
||||
|
|
|
@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, Optional
|
|||
import netaddr
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.gui import nodeutils, validation
|
||||
from core.gui.appconfig import ICONS_PATH
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
|
@ -16,6 +15,7 @@ from core.gui.images import Images
|
|||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import ListboxScroll, image_chooser
|
||||
from core.gui.wrappers import Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -126,12 +126,12 @@ class NodeConfigDialog(Dialog):
|
|||
|
||||
# field frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
# icon field
|
||||
label = ttk.Label(frame, text="Icon")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
self.image_button = ttk.Button(
|
||||
frame,
|
||||
text="Icon",
|
||||
|
@ -139,49 +139,49 @@ class NodeConfigDialog(Dialog):
|
|||
compound=tk.NONE,
|
||||
command=self.click_icon,
|
||||
)
|
||||
self.image_button.grid(row=row, column=1, sticky="ew")
|
||||
self.image_button.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
# name field
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
entry = validation.NodeNameEntry(frame, textvariable=self.name, state=state)
|
||||
entry.grid(row=row, column=1, sticky="ew")
|
||||
entry.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
# node type field
|
||||
if NodeUtils.is_model_node(self.node.type):
|
||||
label = ttk.Label(frame, text="Type")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.type,
|
||||
values=list(NodeUtils.NODE_MODELS),
|
||||
state=combo_state,
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky="ew")
|
||||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
# container image field
|
||||
if NodeUtils.is_image_node(self.node.type):
|
||||
label = ttk.Label(frame, text="Image")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
entry = ttk.Entry(frame, textvariable=self.container_image, state=state)
|
||||
entry.grid(row=row, column=1, sticky="ew")
|
||||
entry.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
if NodeUtils.is_container_node(self.node.type):
|
||||
# server
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Server")
|
||||
label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY)
|
||||
label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY)
|
||||
servers = ["localhost"]
|
||||
servers.extend(list(sorted(self.app.core.servers.keys())))
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.server, values=servers, state=combo_state
|
||||
)
|
||||
combobox.grid(row=row, column=1, sticky="ew")
|
||||
combobox.grid(row=row, column=1, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
if NodeUtils.is_rj45_node(self.node.type):
|
||||
|
@ -190,7 +190,7 @@ class NodeConfigDialog(Dialog):
|
|||
ifaces = ListboxScroll(frame)
|
||||
ifaces.listbox.config(state=state)
|
||||
ifaces.grid(
|
||||
row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
|
||||
row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY
|
||||
)
|
||||
for inf in sorted(response.ifaces[:]):
|
||||
ifaces.listbox.insert(tk.END, inf)
|
||||
|
@ -206,13 +206,13 @@ class NodeConfigDialog(Dialog):
|
|||
|
||||
def draw_ifaces(self) -> None:
|
||||
notebook = ttk.Notebook(self.top)
|
||||
notebook.grid(sticky="nsew", pady=PADY)
|
||||
notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
|
||||
state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
|
||||
for iface_id in sorted(self.canvas_node.ifaces):
|
||||
iface = self.canvas_node.ifaces[iface_id]
|
||||
tab = ttk.Frame(notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew", pady=PADY)
|
||||
tab.grid(sticky=tk.NSEW, pady=PADY)
|
||||
tab.columnconfigure(1, weight=1)
|
||||
tab.columnconfigure(2, weight=1)
|
||||
notebook.add(tab, text=iface.name)
|
||||
|
@ -226,14 +226,16 @@ class NodeConfigDialog(Dialog):
|
|||
text=f"Configure EMANE {emane_model}",
|
||||
command=lambda: self.click_emane_config(emane_model, iface.id),
|
||||
)
|
||||
button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
|
||||
button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY)
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="MAC")
|
||||
label.grid(row=row, column=0, padx=PADX, pady=PADY)
|
||||
auto_set = not iface.mac
|
||||
mac_state = tk.DISABLED if auto_set else tk.NORMAL
|
||||
is_auto = tk.BooleanVar(value=auto_set)
|
||||
mac_state = tk.DISABLED if auto_set else tk.NORMAL
|
||||
if state == tk.DISABLED:
|
||||
mac_state = tk.DISABLED
|
||||
checkbutton = ttk.Checkbutton(
|
||||
tab, text="Auto?", variable=is_auto, state=state
|
||||
)
|
||||
|
@ -241,7 +243,7 @@ class NodeConfigDialog(Dialog):
|
|||
checkbutton.grid(row=row, column=1, padx=PADX)
|
||||
mac = tk.StringVar(value=iface.mac)
|
||||
entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
|
||||
entry.grid(row=row, column=2, sticky="ew")
|
||||
entry.grid(row=row, column=2, sticky=tk.EW)
|
||||
func = partial(mac_auto, is_auto, entry, mac)
|
||||
checkbutton.config(command=func)
|
||||
row += 1
|
||||
|
@ -253,7 +255,7 @@ class NodeConfigDialog(Dialog):
|
|||
ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
|
||||
ip4 = tk.StringVar(value=ip4_net)
|
||||
entry = ttk.Entry(tab, textvariable=ip4, state=state)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
|
||||
row += 1
|
||||
|
||||
label = ttk.Label(tab, text="IPv6")
|
||||
|
@ -263,21 +265,21 @@ class NodeConfigDialog(Dialog):
|
|||
ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
|
||||
ip6 = tk.StringVar(value=ip6_net)
|
||||
entry = ttk.Entry(tab, textvariable=ip6, state=state)
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky="ew")
|
||||
entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW)
|
||||
|
||||
self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_emane_config(self, emane_model: str, iface_id: int) -> None:
|
||||
dialog = EmaneModelDialog(
|
||||
|
|
|
@ -10,25 +10,24 @@ from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
|
|||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll
|
||||
from core.gui.wrappers import Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class NodeConfigServiceDialog(Dialog):
|
||||
def __init__(
|
||||
self, app: "Application", canvas_node: "CanvasNode", services: Set[str] = None
|
||||
self, app: "Application", node: Node, services: Set[str] = None
|
||||
) -> None:
|
||||
title = f"{canvas_node.core_node.name} Config Services"
|
||||
title = f"{node.name} Config Services"
|
||||
super().__init__(app, title)
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node_id: int = canvas_node.core_node.id
|
||||
self.node: Node = node
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
self.services: Optional[CheckboxList] = None
|
||||
self.current: Optional[ListboxScroll] = None
|
||||
if services is None:
|
||||
services = set(canvas_node.core_node.config_services)
|
||||
services = set(node.config_services)
|
||||
self.current_services: Set[str] = services
|
||||
self.draw()
|
||||
|
||||
|
@ -42,32 +41,32 @@ class NodeConfigServiceDialog(Dialog):
|
|||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
self.groups.grid(sticky=tk.NSEW)
|
||||
for group in sorted(self.app.core.config_services_groups):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
self.services.grid(sticky=tk.NSEW)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
self.current.grid(sticky=tk.NSEW)
|
||||
self.draw_current_services()
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
|
@ -75,13 +74,13 @@ class NodeConfigServiceDialog(Dialog):
|
|||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Configure", command=self.click_configure)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.click_remove)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
# trigger group change
|
||||
self.handle_group_change()
|
||||
|
@ -102,7 +101,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.draw_current_services()
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
self.node.config_services = self.current_services.copy()
|
||||
|
||||
def click_configure(self) -> None:
|
||||
current_selection = self.current.listbox.curselection()
|
||||
|
@ -111,8 +110,7 @@ class NodeConfigServiceDialog(Dialog):
|
|||
self,
|
||||
self.app,
|
||||
self.current.listbox.get(current_selection[0]),
|
||||
self.canvas_node,
|
||||
self.node_id,
|
||||
self.node,
|
||||
)
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
@ -132,10 +130,8 @@ class NodeConfigServiceDialog(Dialog):
|
|||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
|
||||
def click_save(self) -> None:
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
logging.info(
|
||||
"saved node config services: %s", self.canvas_node.core_node.config_services
|
||||
)
|
||||
self.node.config_services = self.current_services.copy()
|
||||
logging.info("saved node config services: %s", self.node.config_services)
|
||||
self.destroy()
|
||||
|
||||
def click_cancel(self) -> None:
|
||||
|
@ -154,4 +150,4 @@ class NodeConfigServiceDialog(Dialog):
|
|||
return
|
||||
|
||||
def is_custom_service(self, service: str) -> bool:
|
||||
return service in self.canvas_node.config_service_configs
|
||||
return service in self.node.config_service_configs
|
||||
|
|
|
@ -9,22 +9,21 @@ from core.gui.dialogs.dialog import Dialog
|
|||
from core.gui.dialogs.serviceconfig import ServiceConfigDialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll
|
||||
from core.gui.wrappers import Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class NodeServiceDialog(Dialog):
|
||||
def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None:
|
||||
title = f"{canvas_node.core_node.name} Services"
|
||||
def __init__(self, app: "Application", node: Node) -> None:
|
||||
title = f"{node.name} Services"
|
||||
super().__init__(app, title)
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node_id: int = canvas_node.core_node.id
|
||||
self.node: Node = node
|
||||
self.groups: Optional[ListboxScroll] = None
|
||||
self.services: Optional[CheckboxList] = None
|
||||
self.current: Optional[ListboxScroll] = None
|
||||
services = set(canvas_node.core_node.services)
|
||||
services = set(node.services)
|
||||
self.current_services: Set[str] = services
|
||||
self.draw()
|
||||
|
||||
|
@ -38,31 +37,31 @@ class NodeServiceDialog(Dialog):
|
|||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
self.groups.grid(sticky=tk.NSEW)
|
||||
for group in sorted(self.app.core.services):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
self.services.grid(sticky=tk.NSEW)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
self.current.grid(sticky=tk.NSEW)
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
if self.is_custom_service(service):
|
||||
|
@ -73,13 +72,13 @@ class NodeServiceDialog(Dialog):
|
|||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Configure", command=self.click_configure)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.click_remove)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
# trigger group change
|
||||
self.handle_group_change()
|
||||
|
@ -104,7 +103,7 @@ class NodeServiceDialog(Dialog):
|
|||
self.current.listbox.insert(tk.END, name)
|
||||
if self.is_custom_service(name):
|
||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
self.canvas_node.core_node.services[:] = self.current_services
|
||||
self.node.services = self.current_services.copy()
|
||||
|
||||
def click_configure(self) -> None:
|
||||
current_selection = self.current.listbox.curselection()
|
||||
|
@ -113,8 +112,7 @@ class NodeServiceDialog(Dialog):
|
|||
self,
|
||||
self.app,
|
||||
self.current.listbox.get(current_selection[0]),
|
||||
self.canvas_node,
|
||||
self.node_id,
|
||||
self.node,
|
||||
)
|
||||
|
||||
# if error occurred when creating ServiceConfigDialog, don't show the dialog
|
||||
|
@ -128,8 +126,7 @@ class NodeServiceDialog(Dialog):
|
|||
)
|
||||
|
||||
def click_save(self) -> None:
|
||||
core_node = self.canvas_node.core_node
|
||||
core_node.services[:] = self.current_services
|
||||
self.node.services = self.current_services.copy()
|
||||
self.destroy()
|
||||
|
||||
def click_remove(self) -> None:
|
||||
|
@ -144,6 +141,6 @@ class NodeServiceDialog(Dialog):
|
|||
return
|
||||
|
||||
def is_custom_service(self, service: str) -> bool:
|
||||
has_service_config = service in self.canvas_node.service_configs
|
||||
has_file_config = service in self.canvas_node.service_file_configs
|
||||
has_service_config = service in self.node.service_configs
|
||||
has_file_config = service in self.node.service_file_configs
|
||||
return has_service_config or has_file_config
|
||||
|
|
|
@ -33,60 +33,60 @@ class ObserverDialog(Dialog):
|
|||
|
||||
def draw_listbox(self) -> None:
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(sticky="nsew", pady=PADY)
|
||||
listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
|
||||
listbox_scroll.columnconfigure(0, weight=1)
|
||||
listbox_scroll.rowconfigure(0, weight=1)
|
||||
self.observers = listbox_scroll.listbox
|
||||
self.observers.grid(row=0, column=0, sticky="nsew")
|
||||
self.observers.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.observers.bind("<<ListboxSelect>>", self.handle_observer_change)
|
||||
for name in sorted(self.app.core.custom_observers):
|
||||
self.observers.insert(tk.END, name)
|
||||
|
||||
def draw_form_fields(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
entry.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="Command")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.cmd)
|
||||
entry.grid(row=1, column=1, sticky="ew")
|
||||
entry.grid(row=1, column=1, sticky=tk.EW)
|
||||
|
||||
def draw_config_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.save_button = ttk.Button(
|
||||
frame, text="Save", state=tk.DISABLED, command=self.click_save
|
||||
)
|
||||
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def draw_apply_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save_config)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_save_config(self) -> None:
|
||||
self.app.guiconfig.observers.clear()
|
||||
|
|
|
@ -34,42 +34,42 @@ class PreferencesDialog(Dialog):
|
|||
|
||||
def draw_preferences(self) -> None:
|
||||
frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Theme")
|
||||
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
themes = self.app.style.theme_names()
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.theme, values=themes, state="readonly"
|
||||
)
|
||||
combobox.set(self.theme.get())
|
||||
combobox.grid(row=0, column=1, sticky="ew")
|
||||
combobox.grid(row=0, column=1, sticky=tk.EW)
|
||||
combobox.bind("<<ComboboxSelected>>", self.theme_change)
|
||||
|
||||
label = ttk.Label(frame, text="Editor")
|
||||
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly"
|
||||
)
|
||||
combobox.grid(row=1, column=1, sticky="ew")
|
||||
combobox.grid(row=1, column=1, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="Terminal")
|
||||
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
terminals = sorted(appconfig.TERMINALS.values())
|
||||
combobox = ttk.Combobox(frame, textvariable=self.terminal, values=terminals)
|
||||
combobox.grid(row=2, column=1, sticky="ew")
|
||||
combobox.grid(row=2, column=1, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="3D GUI")
|
||||
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
||||
entry.grid(row=3, column=1, sticky="ew")
|
||||
entry.grid(row=3, column=1, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="Scaling")
|
||||
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
|
||||
scale_frame = ttk.Frame(frame)
|
||||
scale_frame.grid(row=4, column=1, sticky="ew")
|
||||
scale_frame.grid(row=4, column=1, sticky=tk.EW)
|
||||
scale_frame.columnconfigure(0, weight=1)
|
||||
scale = ttk.Scale(
|
||||
scale_frame,
|
||||
|
@ -79,7 +79,7 @@ class PreferencesDialog(Dialog):
|
|||
orient=tk.HORIZONTAL,
|
||||
variable=self.gui_scale,
|
||||
)
|
||||
scale.grid(row=0, column=0, sticky="ew")
|
||||
scale.grid(row=0, column=0, sticky=tk.EW)
|
||||
entry = validation.AppScaleEntry(
|
||||
scale_frame, textvariable=self.gui_scale, width=4
|
||||
)
|
||||
|
@ -90,15 +90,15 @@ class PreferencesDialog(Dialog):
|
|||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def theme_change(self, event: tk.Event) -> None:
|
||||
theme = self.theme.get()
|
||||
|
|
|
@ -25,9 +25,9 @@ class RunToolDialog(Dialog):
|
|||
"""
|
||||
store all CORE nodes (nodes that execute commands) from all existing nodes
|
||||
"""
|
||||
for nid, node in self.app.core.canvas_nodes.items():
|
||||
if NodeUtils.is_container_node(node.core_node.type):
|
||||
self.executable_nodes[node.core_node.name] = nid
|
||||
for node in self.app.core.session.nodes.values():
|
||||
if NodeUtils.is_container_node(node.type):
|
||||
self.executable_nodes[node.name] = node.id
|
||||
|
||||
def draw(self) -> None:
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
@ -38,56 +38,56 @@ class RunToolDialog(Dialog):
|
|||
def draw_command_frame(self) -> None:
|
||||
# the main frame
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(row=0, column=0, sticky="nsew", padx=PADX)
|
||||
frame.grid(row=0, column=0, sticky=tk.NSEW, padx=PADX)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(1, weight=1)
|
||||
|
||||
labeled_frame = ttk.LabelFrame(frame, text="Command", padding=FRAME_PAD)
|
||||
labeled_frame.grid(sticky="ew", pady=PADY)
|
||||
labeled_frame.grid(sticky=tk.EW, pady=PADY)
|
||||
labeled_frame.rowconfigure(0, weight=1)
|
||||
labeled_frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(labeled_frame, textvariable=self.cmd)
|
||||
entry.grid(sticky="ew")
|
||||
entry.grid(sticky=tk.EW)
|
||||
|
||||
# results frame
|
||||
labeled_frame = ttk.LabelFrame(frame, text="Output", padding=FRAME_PAD)
|
||||
labeled_frame.grid(sticky="nsew", pady=PADY)
|
||||
labeled_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
labeled_frame.columnconfigure(0, weight=1)
|
||||
labeled_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.result = CodeText(labeled_frame)
|
||||
self.result.text.config(state=tk.DISABLED, height=15)
|
||||
self.result.grid(sticky="nsew", pady=PADY)
|
||||
self.result.grid(sticky=tk.NSEW, pady=PADY)
|
||||
button_frame = ttk.Frame(labeled_frame)
|
||||
button_frame.grid(sticky="nsew")
|
||||
button_frame.grid(sticky=tk.NSEW)
|
||||
button_frame.columnconfigure(0, weight=1)
|
||||
button_frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(button_frame, text="Run", command=self.click_run)
|
||||
button.grid(sticky="ew", padx=PADX)
|
||||
button.grid(sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(button_frame, text="Close", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def draw_nodes_frame(self) -> None:
|
||||
labeled_frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD)
|
||||
labeled_frame.grid(row=0, column=1, sticky="nsew")
|
||||
labeled_frame.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
labeled_frame.columnconfigure(0, weight=1)
|
||||
labeled_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.node_list = ListboxScroll(labeled_frame)
|
||||
self.node_list.listbox.config(selectmode=tk.MULTIPLE)
|
||||
self.node_list.grid(sticky="nsew", pady=PADY)
|
||||
self.node_list.grid(sticky=tk.NSEW, pady=PADY)
|
||||
for n in sorted(self.executable_nodes.keys()):
|
||||
self.node_list.listbox.insert(tk.END, n)
|
||||
|
||||
button_frame = ttk.Frame(labeled_frame, padding=FRAME_PAD)
|
||||
button_frame.grid(sticky="nsew")
|
||||
button_frame.grid(sticky=tk.NSEW)
|
||||
button_frame.columnconfigure(0, weight=1)
|
||||
button_frame.columnconfigure(1, weight=1)
|
||||
|
||||
button = ttk.Button(button_frame, text="All", command=self.click_all)
|
||||
button.grid(sticky="nsew", padx=PADX)
|
||||
button.grid(sticky=tk.NSEW, padx=PADX)
|
||||
button = ttk.Button(button_frame, text="None", command=self.click_none)
|
||||
button.grid(row=0, column=1, sticky="nsew")
|
||||
button.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
def click_all(self) -> None:
|
||||
self.node_list.listbox.selection_set(0, self.node_list.listbox.size() - 1)
|
||||
|
@ -107,7 +107,7 @@ class RunToolDialog(Dialog):
|
|||
node_name = self.node_list.listbox.get(selection)
|
||||
node_id = self.executable_nodes[node_name]
|
||||
response = self.app.core.client.node_command(
|
||||
self.app.core.session_id, node_id, command
|
||||
self.app.core.session.id, node_id, command
|
||||
)
|
||||
self.result.text.insert(
|
||||
tk.END, f"> {node_name} > {command}:\n{response.output}\n"
|
||||
|
|
|
@ -37,12 +37,12 @@ class ServersDialog(Dialog):
|
|||
|
||||
def draw_servers(self) -> None:
|
||||
listbox_scroll = ListboxScroll(self.top)
|
||||
listbox_scroll.grid(pady=PADY, sticky="nsew")
|
||||
listbox_scroll.grid(pady=PADY, sticky=tk.NSEW)
|
||||
listbox_scroll.columnconfigure(0, weight=1)
|
||||
listbox_scroll.rowconfigure(0, weight=1)
|
||||
|
||||
self.servers = listbox_scroll.listbox
|
||||
self.servers.grid(row=0, column=0, sticky="nsew")
|
||||
self.servers.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.servers.bind("<<ListboxSelect>>", self.handle_server_change)
|
||||
|
||||
for server in self.app.core.servers:
|
||||
|
@ -50,52 +50,52 @@ class ServersDialog(Dialog):
|
|||
|
||||
def draw_server_configuration(self) -> None:
|
||||
frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.grid(pady=PADY, sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(3, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Name")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.name)
|
||||
entry.grid(row=0, column=1, sticky="ew")
|
||||
entry.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="Address")
|
||||
label.grid(row=0, column=2, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=2, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.address)
|
||||
entry.grid(row=0, column=3, sticky="ew")
|
||||
entry.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def draw_servers_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(pady=PADY, sticky="ew")
|
||||
frame.grid(pady=PADY, sticky=tk.EW)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(frame, text="Create", command=self.click_create)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.save_button = ttk.Button(
|
||||
frame, text="Save", state=tk.DISABLED, command=self.click_save
|
||||
)
|
||||
self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.save_button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
|
||||
self.delete_button = ttk.Button(
|
||||
frame, text="Delete", state=tk.DISABLED, command=self.click_delete
|
||||
)
|
||||
self.delete_button.grid(row=0, column=2, sticky="ew")
|
||||
self.delete_button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def draw_apply_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
button = ttk.Button(
|
||||
frame, text="Save Configuration", command=self.click_save_configuration
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_save_configuration(self):
|
||||
self.app.guiconfig.servers.clear()
|
||||
|
|
|
@ -7,16 +7,15 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
|||
import grpc
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.services_pb2 import NodeServiceData, ServiceValidationMode
|
||||
from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ListboxScroll
|
||||
from core.gui.wrappers import Node, NodeServiceData, ServiceValidationMode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.coreclient import CoreClient
|
||||
|
||||
ICON_SIZE: int = 16
|
||||
|
@ -24,18 +23,12 @@ ICON_SIZE: int = 16
|
|||
|
||||
class ServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.BaseWidget,
|
||||
app: "Application",
|
||||
service_name: str,
|
||||
canvas_node: "CanvasNode",
|
||||
node_id: int,
|
||||
self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
|
||||
) -> None:
|
||||
title = f"{service_name} Service"
|
||||
super().__init__(app, title, master=master)
|
||||
self.core: "CoreClient" = app.core
|
||||
self.canvas_node: "CanvasNode" = canvas_node
|
||||
self.node_id: int = node_id
|
||||
self.node: Node = node
|
||||
self.service_name: str = service_name
|
||||
self.radiovar: tk.IntVar = tk.IntVar(value=2)
|
||||
self.metadata: str = ""
|
||||
|
@ -72,7 +65,7 @@ class ServiceConfigDialog(Dialog):
|
|||
self.service_file_data: Optional[CodeText] = None
|
||||
self.validation_period_entry: Optional[ttk.Entry] = None
|
||||
self.original_service_files: Dict[str, str] = {}
|
||||
self.default_config: NodeServiceData = None
|
||||
self.default_config: Optional[NodeServiceData] = None
|
||||
self.temp_service_files: Dict[str, str] = {}
|
||||
self.modified_files: Set[str] = set()
|
||||
self.has_error: bool = False
|
||||
|
@ -84,15 +77,13 @@ class ServiceConfigDialog(Dialog):
|
|||
try:
|
||||
self.app.core.create_nodes_and_links()
|
||||
default_config = self.app.core.get_node_service(
|
||||
self.node_id, self.service_name
|
||||
self.node.id, self.service_name
|
||||
)
|
||||
self.default_startup = default_config.startup[:]
|
||||
self.default_validate = default_config.validate[:]
|
||||
self.default_shutdown = default_config.shutdown[:]
|
||||
self.default_directories = default_config.dirs[:]
|
||||
custom_service_config = self.canvas_node.service_configs.get(
|
||||
self.service_name
|
||||
)
|
||||
custom_service_config = self.node.service_configs.get(self.service_name)
|
||||
self.default_config = default_config
|
||||
service_config = (
|
||||
custom_service_config if custom_service_config else default_config
|
||||
|
@ -109,15 +100,13 @@ class ServiceConfigDialog(Dialog):
|
|||
self.temp_directories = service_config.dirs[:]
|
||||
self.original_service_files = {
|
||||
x: self.app.core.get_node_service_file(
|
||||
self.node_id, self.service_name, x
|
||||
self.node.id, self.service_name, x
|
||||
)
|
||||
for x in default_config.configs
|
||||
}
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
|
||||
file_configs = self.canvas_node.service_file_configs.get(
|
||||
self.service_name, {}
|
||||
)
|
||||
file_configs = self.node.service_file_configs.get(self.service_name, {})
|
||||
for file, data in file_configs.items():
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
|
@ -130,16 +119,16 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
# draw metadata
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Meta-data")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata)
|
||||
self.metadata_entry.grid(row=0, column=1, sticky="ew")
|
||||
self.metadata_entry.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.notebook.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_tab_files()
|
||||
self.draw_tab_directories()
|
||||
self.draw_tab_startstop()
|
||||
|
@ -149,7 +138,7 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_files(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Files")
|
||||
|
||||
|
@ -159,15 +148,15 @@ class ServiceConfigDialog(Dialog):
|
|||
label.grid()
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="File Name")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky=tk.W)
|
||||
self.filename_combobox = ttk.Combobox(frame, values=self.filenames)
|
||||
self.filename_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.display_service_file_data
|
||||
)
|
||||
self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.filename_combobox.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(
|
||||
frame, image=self.documentnew_img, command=self.add_filename
|
||||
)
|
||||
|
@ -178,7 +167,7 @@ class ServiceConfigDialog(Dialog):
|
|||
button.grid(row=0, column=3)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Radiobutton(
|
||||
frame,
|
||||
|
@ -187,16 +176,16 @@ class ServiceConfigDialog(Dialog):
|
|||
value=1,
|
||||
state=tk.DISABLED,
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
entry = ttk.Entry(frame, state=tk.DISABLED)
|
||||
entry.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
button = ttk.Radiobutton(
|
||||
frame,
|
||||
|
@ -204,7 +193,7 @@ class ServiceConfigDialog(Dialog):
|
|||
text="Use text below for file contents",
|
||||
value=2,
|
||||
)
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
button.grid(row=0, column=0, sticky=tk.EW)
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
button = ttk.Button(frame, image=image)
|
||||
button.image = image
|
||||
|
@ -215,7 +204,7 @@ class ServiceConfigDialog(Dialog):
|
|||
button.grid(row=0, column=2)
|
||||
|
||||
self.service_file_data = CodeText(tab)
|
||||
self.service_file_data.grid(sticky="nsew")
|
||||
self.service_file_data.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1)
|
||||
if len(self.filenames) > 0:
|
||||
self.filename_combobox.current(0)
|
||||
|
@ -229,7 +218,7 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_directories(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
tab.rowconfigure(2, weight=1)
|
||||
self.notebook.add(tab, text="Directories")
|
||||
|
@ -238,33 +227,33 @@ class ServiceConfigDialog(Dialog):
|
|||
tab,
|
||||
text="Directories required by this service that are unique for each node.",
|
||||
)
|
||||
label.grid(row=0, column=0, sticky="ew")
|
||||
label.grid(row=0, column=0, sticky=tk.EW)
|
||||
frame = ttk.Frame(tab, padding=FRAME_PAD)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.grid(row=1, column=0, sticky="nsew")
|
||||
frame.grid(row=1, column=0, sticky=tk.NSEW)
|
||||
var = tk.StringVar(value="")
|
||||
self.directory_entry = ttk.Entry(frame, textvariable=var)
|
||||
self.directory_entry.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
self.directory_entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="...", command=self.find_directory_button)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
self.dir_list = ListboxScroll(tab)
|
||||
self.dir_list.grid(row=2, column=0, sticky="nsew", pady=PADY)
|
||||
self.dir_list.grid(row=2, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
self.dir_list.listbox.bind("<<ListboxSelect>>", self.directory_select)
|
||||
for d in self.temp_directories:
|
||||
self.dir_list.listbox.insert("end", d)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(row=3, column=0, sticky="nsew")
|
||||
frame.grid(row=3, column=0, sticky=tk.NSEW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Add", command=self.add_directory)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.remove_directory)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def draw_tab_startstop(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
|
@ -290,25 +279,25 @@ class ServiceConfigDialog(Dialog):
|
|||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(1, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(row=0, column=0, sticky="nsew", pady=PADY)
|
||||
frame.grid(row=0, column=0, sticky=tk.NSEW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(frame, textvariable=tk.StringVar())
|
||||
entry.grid(row=0, column=0, stick="ew", padx=PADX)
|
||||
button = ttk.Button(frame, image=self.documentnew_img)
|
||||
button.bind("<Button-1>", self.add_command)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, image=self.editdelete_img)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
button.bind("<Button-1>", self.delete_command)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.listbox.bind("<<ListboxSelect>>", self.update_entry)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(row=1, column=0, sticky="nsew")
|
||||
listbox_scroll.grid(row=1, column=0, sticky=tk.NSEW)
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
|
@ -318,23 +307,23 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
def draw_tab_configuration(self) -> None:
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.grid(sticky=tk.NSEW)
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration", sticky="nsew")
|
||||
self.notebook.add(tab, text="Configuration", sticky=tk.NSEW)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
if self.validation_mode == ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == ServiceValidationMode.NON_BLOCKING:
|
||||
|
@ -346,48 +335,48 @@ class ServiceConfigDialog(Dialog):
|
|||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=2, column=0, sticky=tk.W, padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=tk.StringVar()
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
listbox_scroll.grid(sticky=tk.NSEW)
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=2, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def add_filename(self) -> None:
|
||||
filename = self.filename_combobox.get()
|
||||
|
@ -453,7 +442,7 @@ class ServiceConfigDialog(Dialog):
|
|||
and not self.has_new_files()
|
||||
and not self.is_custom_directory()
|
||||
):
|
||||
self.canvas_node.service_configs.pop(self.service_name, None)
|
||||
self.node.service_configs.pop(self.service_name, None)
|
||||
self.current_service_color("")
|
||||
self.destroy()
|
||||
return
|
||||
|
@ -466,7 +455,7 @@ class ServiceConfigDialog(Dialog):
|
|||
):
|
||||
startup, validate, shutdown = self.get_commands()
|
||||
config = self.core.set_node_service(
|
||||
self.node_id,
|
||||
self.node.id,
|
||||
self.service_name,
|
||||
dirs=self.temp_directories,
|
||||
files=list(self.filename_combobox["values"]),
|
||||
|
@ -474,15 +463,15 @@ class ServiceConfigDialog(Dialog):
|
|||
validations=validate,
|
||||
shutdowns=shutdown,
|
||||
)
|
||||
self.canvas_node.service_configs[self.service_name] = config
|
||||
self.node.service_configs[self.service_name] = config
|
||||
for file in self.modified_files:
|
||||
file_configs = self.canvas_node.service_file_configs.setdefault(
|
||||
file_configs = self.node.service_file_configs.setdefault(
|
||||
self.service_name, {}
|
||||
)
|
||||
file_configs[file] = self.temp_service_files[file]
|
||||
# TODO: check if this is really needed
|
||||
self.app.core.set_node_service_file(
|
||||
self.node_id, self.service_name, file, self.temp_service_files[file]
|
||||
self.node.id, self.service_name, file, self.temp_service_files[file]
|
||||
)
|
||||
self.current_service_color("green")
|
||||
except grpc.RpcError as e:
|
||||
|
@ -526,8 +515,8 @@ class ServiceConfigDialog(Dialog):
|
|||
clears out any custom configuration permanently
|
||||
"""
|
||||
# clear coreclient data
|
||||
self.canvas_node.service_configs.pop(self.service_name, None)
|
||||
file_configs = self.canvas_node.service_file_configs.pop(self.service_name, {})
|
||||
self.node.service_configs.pop(self.service_name, None)
|
||||
file_configs = self.node.service_file_configs.pop(self.service_name, {})
|
||||
file_configs.pop(self.service_name, None)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
self.modified_files.clear()
|
||||
|
@ -564,9 +553,8 @@ class ServiceConfigDialog(Dialog):
|
|||
|
||||
def click_copy(self) -> None:
|
||||
file_name = self.filename_combobox.get()
|
||||
name = self.canvas_node.core_node.name
|
||||
dialog = CopyServiceConfigDialog(
|
||||
self.app, self, name, self.service_name, file_name
|
||||
self.app, self, self.node.name, self.service_name, file_name
|
||||
)
|
||||
dialog.show()
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ from typing import TYPE_CHECKING, Dict, Optional
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
from core.gui.wrappers import ConfigOption
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -26,9 +26,9 @@ class SessionOptionsDialog(Dialog):
|
|||
|
||||
def get_config(self) -> Dict[str, ConfigOption]:
|
||||
try:
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
response = self.app.core.client.get_session_options(session_id)
|
||||
return response.config
|
||||
return ConfigOption.from_dict(response.config)
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Session Options Error", e)
|
||||
self.has_error = True
|
||||
|
@ -39,22 +39,22 @@ class SessionOptionsDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
button = ttk.Button(frame, text="Save", command=self.save, state=state)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def save(self) -> None:
|
||||
config = self.config_frame.parse_config()
|
||||
try:
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
response = self.app.core.client.set_session_options(session_id, config)
|
||||
logging.info("saved session config: %s", response)
|
||||
except grpc.RpcError as e:
|
||||
|
|
|
@ -5,12 +5,11 @@ from typing import TYPE_CHECKING, List, Optional
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.core_pb2 import SessionSummary
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.task import ProgressTask
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.wrappers import SessionState, SessionSummary
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -33,7 +32,7 @@ class SessionsDialog(Dialog):
|
|||
try:
|
||||
response = self.app.core.client.get_sessions()
|
||||
logging.info("sessions: %s", response)
|
||||
return response.sessions
|
||||
return [SessionSummary.from_proto(x) for x in response.sessions]
|
||||
except grpc.RpcError as e:
|
||||
self.app.show_grpc_exception("Get Sessions Error", e)
|
||||
self.destroy()
|
||||
|
@ -63,7 +62,7 @@ class SessionsDialog(Dialog):
|
|||
frame = ttk.Frame(self.top)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.tree = ttk.Treeview(
|
||||
frame,
|
||||
columns=("id", "state", "nodes"),
|
||||
|
@ -73,7 +72,7 @@ class SessionsDialog(Dialog):
|
|||
style = ttk.Style()
|
||||
heading_size = int(self.app.guiconfig.scale * 10)
|
||||
style.configure("Treeview.Heading", font=(None, heading_size, "bold"))
|
||||
self.tree.grid(sticky="nsew")
|
||||
self.tree.grid(sticky=tk.NSEW)
|
||||
self.tree.column("id", stretch=tk.YES, anchor="center")
|
||||
self.tree.heading("id", text="ID")
|
||||
self.tree.column("state", stretch=tk.YES, anchor="center")
|
||||
|
@ -82,7 +81,7 @@ class SessionsDialog(Dialog):
|
|||
self.tree.heading("nodes", text="Node Count")
|
||||
|
||||
for index, session in enumerate(self.sessions):
|
||||
state_name = core_pb2.SessionState.Enum.Name(session.state)
|
||||
state_name = SessionState(session.state).name
|
||||
self.tree.insert(
|
||||
"",
|
||||
tk.END,
|
||||
|
@ -93,25 +92,25 @@ class SessionsDialog(Dialog):
|
|||
self.tree.bind("<<TreeviewSelect>>", self.click_select)
|
||||
|
||||
yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.tree.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
|
||||
xscrollbar.grid(row=1, sticky="ew")
|
||||
xscrollbar.grid(row=1, sticky=tk.EW)
|
||||
self.tree.configure(xscrollcommand=xscrollbar.set)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.DOCUMENTNEW, 16)
|
||||
b = ttk.Button(
|
||||
frame, image=image, text="New", compound=tk.LEFT, command=self.click_new
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, padx=PADX, sticky="ew")
|
||||
b.grid(row=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.FILEOPEN, 16)
|
||||
self.connect_button = ttk.Button(
|
||||
|
@ -123,7 +122,7 @@ class SessionsDialog(Dialog):
|
|||
state=tk.DISABLED,
|
||||
)
|
||||
self.connect_button.image = image
|
||||
self.connect_button.grid(row=0, column=1, padx=PADX, sticky="ew")
|
||||
self.connect_button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.DELETE, 16)
|
||||
self.delete_button = ttk.Button(
|
||||
|
@ -135,7 +134,7 @@ class SessionsDialog(Dialog):
|
|||
state=tk.DISABLED,
|
||||
)
|
||||
self.delete_button.image = image
|
||||
self.delete_button.grid(row=0, column=2, padx=PADX, sticky="ew")
|
||||
self.delete_button.grid(row=0, column=2, padx=PADX, sticky=tk.EW)
|
||||
|
||||
image = Images.get(ImageEnum.CANCEL, 16)
|
||||
if self.is_start_app:
|
||||
|
@ -155,7 +154,7 @@ class SessionsDialog(Dialog):
|
|||
command=self.destroy,
|
||||
)
|
||||
b.image = image
|
||||
b.grid(row=0, column=3, sticky="ew")
|
||||
b.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def click_new(self) -> None:
|
||||
self.app.core.create_new_session()
|
||||
|
@ -182,8 +181,6 @@ class SessionsDialog(Dialog):
|
|||
|
||||
def join_session(self, session_id: int) -> None:
|
||||
self.destroy()
|
||||
if self.app.core.xml_file:
|
||||
self.app.core.xml_file = None
|
||||
task = ProgressTask(
|
||||
self.app, "Join", self.app.core.join_session, args=(session_id,)
|
||||
)
|
||||
|
@ -191,7 +188,7 @@ class SessionsDialog(Dialog):
|
|||
|
||||
def double_click_join(self, _event: tk.Event) -> None:
|
||||
item = self.tree.selection()
|
||||
if item is None:
|
||||
if not item:
|
||||
return
|
||||
session_id = int(self.tree.item(item, "text"))
|
||||
self.join_session(session_id)
|
||||
|
@ -202,7 +199,7 @@ class SessionsDialog(Dialog):
|
|||
logging.debug("delete session: %s", self.selected_session)
|
||||
self.tree.delete(self.selected_id)
|
||||
self.app.core.delete_session(self.selected_session)
|
||||
if self.selected_session == self.app.core.session_id:
|
||||
if self.selected_session == self.app.core.session.id:
|
||||
self.click_new()
|
||||
self.destroy()
|
||||
self.click_select()
|
||||
|
|
|
@ -57,15 +57,15 @@ class ShapeDialog(Dialog):
|
|||
|
||||
def draw_label_options(self) -> None:
|
||||
label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
entry = ttk.Entry(label_frame, textvariable=self.shape_text)
|
||||
entry.grid(sticky="ew", pady=PADY)
|
||||
entry.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
# font options
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="nsew", pady=PADY)
|
||||
frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
frame.columnconfigure(2, weight=1)
|
||||
|
@ -75,70 +75,70 @@ class ShapeDialog(Dialog):
|
|||
values=sorted(font.families()),
|
||||
state="readonly",
|
||||
)
|
||||
combobox.grid(row=0, column=0, sticky="nsew")
|
||||
combobox.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=1, padx=PADX, sticky="nsew")
|
||||
combobox.grid(row=0, column=1, padx=PADX, sticky=tk.NSEW)
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_text_color)
|
||||
button.grid(row=0, column=2, sticky="nsew")
|
||||
button.grid(row=0, column=2, sticky=tk.NSEW)
|
||||
|
||||
# style options
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Checkbutton(frame, variable=self.bold, text="Bold")
|
||||
button.grid(row=0, column=0, sticky="ew")
|
||||
button.grid(row=0, column=0, sticky=tk.EW)
|
||||
button = ttk.Checkbutton(frame, variable=self.italic, text="Italic")
|
||||
button.grid(row=0, column=1, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=1, padx=PADX, sticky=tk.EW)
|
||||
button = ttk.Checkbutton(frame, variable=self.underline, text="Underline")
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
def draw_shape_options(self) -> None:
|
||||
label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="ew", pady=PADY)
|
||||
label_frame.grid(sticky=tk.EW, pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(1, 3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label = ttk.Label(frame, text="Fill Color")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky="w")
|
||||
label.grid(row=0, column=0, padx=PADX, sticky=tk.W)
|
||||
self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color)
|
||||
self.fill.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
self.fill.grid(row=0, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_fill_color)
|
||||
button.grid(row=0, column=2, sticky="ew")
|
||||
button.grid(row=0, column=2, sticky=tk.EW)
|
||||
|
||||
label = ttk.Label(frame, text="Border Color")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.W, padx=PADX)
|
||||
self.border = ttk.Label(
|
||||
frame, text=self.border_color, background=self.border_color
|
||||
)
|
||||
self.border.grid(row=1, column=1, sticky="ew", padx=PADX)
|
||||
self.border.grid(row=1, column=1, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Color", command=self.choose_border_color)
|
||||
button.grid(row=1, column=2, sticky="ew")
|
||||
button.grid(row=1, column=2, sticky=tk.EW)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.grid(sticky=tk.EW, pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Border Width")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.W, padx=PADX)
|
||||
combobox = ttk.Combobox(
|
||||
frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly"
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky="nsew")
|
||||
combobox.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
def draw_buttons(self) -> None:
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="nsew")
|
||||
frame.grid(sticky=tk.NSEW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
button = ttk.Button(frame, text="Add shape", command=self.click_add)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.cancel)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def choose_text_color(self) -> None:
|
||||
color_picker = ColorPickerDialog(self, self.app, self.text_color)
|
||||
|
|
|
@ -37,25 +37,25 @@ class ThroughputDialog(Dialog):
|
|||
variable=self.show_throughput,
|
||||
text="Show Throughput Level On Every Link",
|
||||
)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
button = ttk.Checkbutton(
|
||||
self.top,
|
||||
variable=self.exponential_weight,
|
||||
text="Use Exponential Weighted Moving Average",
|
||||
)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
button = ttk.Checkbutton(
|
||||
self.top, variable=self.transmission, text="Include Transmissions"
|
||||
)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
button = ttk.Checkbutton(
|
||||
self.top, variable=self.reception, text="Include Receptions"
|
||||
)
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
|
||||
label_frame = ttk.LabelFrame(self.top, text="Link Highlight", padding=FRAME_PAD)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.grid(sticky="ew")
|
||||
label_frame.grid(sticky=tk.EW)
|
||||
|
||||
scale = ttk.Scale(
|
||||
label_frame,
|
||||
|
@ -65,21 +65,21 @@ class ThroughputDialog(Dialog):
|
|||
orient=tk.HORIZONTAL,
|
||||
variable=self.threshold,
|
||||
)
|
||||
scale.grid(sticky="ew", pady=PADY)
|
||||
scale.grid(sticky=tk.EW, pady=PADY)
|
||||
|
||||
frame = ttk.Frame(label_frame)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Threshold Kbps (0 disabled)")
|
||||
label.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
label.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.threshold)
|
||||
entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
|
||||
label = ttk.Label(frame, text="Width")
|
||||
label.grid(row=1, column=0, sticky="ew", padx=PADX)
|
||||
label.grid(row=1, column=0, sticky=tk.EW, padx=PADX)
|
||||
entry = ttk.Entry(frame, textvariable=self.width)
|
||||
entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
entry.grid(row=1, column=1, sticky=tk.EW, pady=PADY)
|
||||
label = ttk.Label(frame, text="Color")
|
||||
label.grid(row=2, column=0, sticky="ew", padx=PADX)
|
||||
label.grid(row=2, column=0, sticky=tk.EW, padx=PADX)
|
||||
self.color_button = tk.Button(
|
||||
frame,
|
||||
text=self.color,
|
||||
|
@ -87,18 +87,18 @@ class ThroughputDialog(Dialog):
|
|||
bg=self.color,
|
||||
highlightthickness=0,
|
||||
)
|
||||
self.color_button.grid(row=2, column=1, sticky="ew")
|
||||
self.color_button.grid(row=2, column=1, sticky=tk.EW)
|
||||
|
||||
self.draw_spacer()
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_color(self) -> None:
|
||||
color_picker = ColorPickerDialog(self, self.app, self.color)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Node
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import PADX, PADY
|
||||
from core.gui.widgets import ConfigFrame
|
||||
from core.gui.wrappers import ConfigOption, Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -30,7 +30,7 @@ class WlanConfigDialog(Dialog):
|
|||
self.ranges: Dict[int, int] = {}
|
||||
self.positive_int: int = self.app.master.register(self.validate_and_update)
|
||||
try:
|
||||
config = self.canvas_node.wlan_config
|
||||
config = self.node.wlan_config
|
||||
if not config:
|
||||
config = self.app.core.get_wlan_config(self.node.id)
|
||||
self.config: Dict[str, ConfigOption] = config
|
||||
|
@ -55,7 +55,7 @@ class WlanConfigDialog(Dialog):
|
|||
self.top.rowconfigure(0, weight=1)
|
||||
self.config_frame = ConfigFrame(self.top, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
|
||||
self.draw_apply_buttons()
|
||||
self.top.bind("<Destroy>", self.remove_ranges)
|
||||
|
||||
|
@ -64,7 +64,7 @@ class WlanConfigDialog(Dialog):
|
|||
create node configuration options
|
||||
"""
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
for i in range(2):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
|
@ -74,19 +74,19 @@ class WlanConfigDialog(Dialog):
|
|||
self.range_entry.config(validatecommand=(self.positive_int, "%P"))
|
||||
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, padx=PADX, sticky="ew")
|
||||
button.grid(row=0, column=0, padx=PADX, sticky=tk.EW)
|
||||
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=1, sticky="ew")
|
||||
button.grid(row=0, column=1, sticky=tk.EW)
|
||||
|
||||
def click_apply(self) -> None:
|
||||
"""
|
||||
retrieve user's wlan configuration and store the new configuration values
|
||||
"""
|
||||
config = self.config_frame.parse_config()
|
||||
self.canvas_node.wlan_config = self.config
|
||||
self.node.wlan_config = self.config
|
||||
if self.app.core.is_runtime():
|
||||
session_id = self.app.core.session_id
|
||||
session_id = self.app.core.session.id
|
||||
self.app.core.client.set_wlan_config(session_id, self.node.id, config)
|
||||
self.remove_ranges()
|
||||
self.destroy()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.api.grpc.core_pb2 import Interface
|
||||
from core.gui.frames.base import DetailsFrame, InfoFrameBase
|
||||
from core.gui.utils import bandwidth_text
|
||||
from core.gui.wrappers import Interface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -34,13 +34,11 @@ class EdgeInfoFrame(InfoFrameBase):
|
|||
self.columnconfigure(0, weight=1)
|
||||
link = self.edge.link
|
||||
options = link.options
|
||||
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
|
||||
src_node = src_canvas_node.core_node
|
||||
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
|
||||
dst_node = dst_canvas_node.core_node
|
||||
src_node = self.app.core.session.nodes[link.node1_id]
|
||||
dst_node = self.app.core.session.nodes[link.node2_id]
|
||||
|
||||
frame = DetailsFrame(self)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.add_detail("Source", src_node.name)
|
||||
iface1 = link.iface1
|
||||
if iface1:
|
||||
|
@ -62,7 +60,7 @@ class EdgeInfoFrame(InfoFrameBase):
|
|||
ip6 = f"{iface2.ip6}/{iface2.ip6_mask}" if iface2.ip6 else ""
|
||||
frame.add_detail("IP6", ip6)
|
||||
|
||||
if link.HasField("options"):
|
||||
if link.options:
|
||||
frame.add_separator()
|
||||
bandwidth = bandwidth_text(options.bandwidth)
|
||||
frame.add_detail("Bandwidth", bandwidth)
|
||||
|
@ -81,9 +79,9 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
|
|||
|
||||
def draw(self) -> None:
|
||||
link = self.edge.link
|
||||
src_canvas_node = self.app.core.canvas_nodes[link.node1_id]
|
||||
src_canvas_node = self.app.canvas.nodes[self.edge.src]
|
||||
src_node = src_canvas_node.core_node
|
||||
dst_canvas_node = self.app.core.canvas_nodes[link.node2_id]
|
||||
dst_canvas_node = self.app.canvas.nodes[self.edge.dst]
|
||||
dst_node = dst_canvas_node.core_node
|
||||
|
||||
# find interface for each node connected to network
|
||||
|
@ -92,7 +90,7 @@ class WirelessEdgeInfoFrame(InfoFrameBase):
|
|||
iface2 = get_iface(dst_canvas_node, net_id)
|
||||
|
||||
frame = DetailsFrame(self)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.add_detail("Source", src_node.name)
|
||||
if iface1:
|
||||
mac = iface1.mac if iface1.mac else "auto"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.api.grpc.core_pb2 import NodeType
|
||||
from core.gui.frames.base import DetailsFrame, InfoFrameBase
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.wrappers import NodeType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -18,7 +19,7 @@ class NodeInfoFrame(InfoFrameBase):
|
|||
self.columnconfigure(0, weight=1)
|
||||
node = self.canvas_node.core_node
|
||||
frame = DetailsFrame(self)
|
||||
frame.grid(sticky="ew")
|
||||
frame.grid(sticky=tk.EW)
|
||||
frame.add_detail("ID", node.id)
|
||||
frame.add_detail("Name", node.name)
|
||||
if NodeUtils.is_model_node(node.type):
|
||||
|
|
|
@ -3,14 +3,13 @@ import math
|
|||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.core_pb2 import Interface, Link
|
||||
from core.gui import themes
|
||||
from core.gui.dialogs.linkconfig import LinkConfigurationDialog
|
||||
from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame
|
||||
from core.gui.graph import tags
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.utils import bandwidth_text
|
||||
from core.gui.utils import bandwidth_text, delay_jitter_text
|
||||
from core.gui.wrappers import Interface, Link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.graph.graph import CanvasGraph
|
||||
|
@ -111,7 +110,9 @@ class Edge:
|
|||
arc_y = (perp_m * arc_x) + b
|
||||
return arc_x, arc_y
|
||||
|
||||
def draw(self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float]) -> None:
|
||||
def draw(
|
||||
self, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], state: str
|
||||
) -> None:
|
||||
arc_pos = self._get_arcpoint(src_pos, dst_pos)
|
||||
self.id = self.canvas.create_line(
|
||||
*src_pos,
|
||||
|
@ -121,6 +122,7 @@ class Edge:
|
|||
tags=self.tag,
|
||||
width=self.scaled_width(),
|
||||
fill=self.color,
|
||||
state=state,
|
||||
)
|
||||
|
||||
def redraw(self) -> None:
|
||||
|
@ -250,7 +252,7 @@ class CanvasWirelessEdge(Edge):
|
|||
self.width: float = WIRELESS_WIDTH
|
||||
color = link.color if link.color else WIRELESS_COLOR
|
||||
self.color: str = color
|
||||
self.draw(src_pos, dst_pos)
|
||||
self.draw(src_pos, dst_pos, self.canvas.show_wireless.state())
|
||||
if link.label:
|
||||
self.middle_label_text(link.label)
|
||||
self.set_binding()
|
||||
|
@ -287,7 +289,7 @@ class CanvasEdge(Edge):
|
|||
self.link: Optional[Link] = None
|
||||
self.asymmetric_link: Optional[Link] = None
|
||||
self.throughput: Optional[float] = None
|
||||
self.draw(src_pos, dst_pos)
|
||||
self.draw(src_pos, dst_pos, tk.NORMAL)
|
||||
self.set_binding()
|
||||
self.context: tk.Menu = tk.Menu(self.canvas)
|
||||
self.create_context()
|
||||
|
@ -305,7 +307,7 @@ class CanvasEdge(Edge):
|
|||
self.link = link
|
||||
self.draw_labels()
|
||||
|
||||
def iface_label(self, iface: core_pb2.Interface) -> str:
|
||||
def iface_label(self, iface: Interface) -> str:
|
||||
label = ""
|
||||
if iface.name and self.canvas.show_iface_names.get():
|
||||
label = f"{iface.name}"
|
||||
|
@ -319,10 +321,10 @@ class CanvasEdge(Edge):
|
|||
|
||||
def create_node_labels(self) -> Tuple[str, str]:
|
||||
label1 = None
|
||||
if self.link.HasField("iface1"):
|
||||
if self.link.iface1:
|
||||
label1 = self.iface_label(self.link.iface1)
|
||||
label2 = None
|
||||
if self.link.HasField("iface2"):
|
||||
if self.link.iface2:
|
||||
label2 = self.iface_label(self.link.iface2)
|
||||
return label1, label2
|
||||
|
||||
|
@ -379,6 +381,7 @@ class CanvasEdge(Edge):
|
|||
def check_wireless(self) -> None:
|
||||
if self.is_wireless():
|
||||
self.canvas.itemconfig(self.id, state=tk.HIDDEN)
|
||||
self.canvas.dtag(self.id, tags.EDGE)
|
||||
self._check_antenna()
|
||||
|
||||
def _check_antenna(self) -> None:
|
||||
|
@ -417,22 +420,38 @@ class CanvasEdge(Edge):
|
|||
dialog.show()
|
||||
|
||||
def draw_link_options(self):
|
||||
if not self.link.options:
|
||||
return
|
||||
options = self.link.options
|
||||
asym_options = None
|
||||
if self.asymmetric_link and self.asymmetric_link.options:
|
||||
asym_options = self.asymmetric_link.options
|
||||
lines = []
|
||||
bandwidth = options.bandwidth
|
||||
if bandwidth > 0:
|
||||
lines.append(bandwidth_text(bandwidth))
|
||||
delay = options.delay
|
||||
jitter = options.jitter
|
||||
if delay > 0 and jitter > 0:
|
||||
lines.append(f"{delay} us (\u00B1{jitter} us)")
|
||||
elif jitter > 0:
|
||||
lines.append(f"0 us (\u00B1{jitter} us)")
|
||||
loss = options.loss
|
||||
if loss > 0:
|
||||
lines.append(f"loss={loss}%")
|
||||
dup = options.dup
|
||||
if dup > 0:
|
||||
lines.append(f"dup={dup}%")
|
||||
# bandwidth
|
||||
if options.bandwidth > 0:
|
||||
bandwidth_line = bandwidth_text(options.bandwidth)
|
||||
if asym_options and asym_options.bandwidth > 0:
|
||||
bandwidth_line += f" / {bandwidth_text(asym_options.bandwidth)}"
|
||||
lines.append(bandwidth_line)
|
||||
# delay/jitter
|
||||
dj_line = delay_jitter_text(options.delay, options.jitter)
|
||||
if dj_line and asym_options:
|
||||
asym_dj_line = delay_jitter_text(asym_options.delay, asym_options.jitter)
|
||||
if asym_dj_line:
|
||||
dj_line += f" / {asym_dj_line}"
|
||||
if dj_line:
|
||||
lines.append(dj_line)
|
||||
# loss
|
||||
if options.loss > 0:
|
||||
loss_line = f"loss={options.loss}%"
|
||||
if asym_options and asym_options.loss > 0:
|
||||
loss_line += f" / loss={asym_options.loss}%"
|
||||
lines.append(loss_line)
|
||||
# duplicate
|
||||
if options.dup > 0:
|
||||
dup_line = f"dup={options.dup}%"
|
||||
if asym_options and asym_options.dup > 0:
|
||||
dup_line += f" / dup={asym_options.dup}%"
|
||||
lines.append(dup_line)
|
||||
label = "\n".join(lines)
|
||||
self.middle_label_text(label)
|
||||
|
|
|
@ -7,14 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
|||
from PIL import Image
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.core_pb2 import (
|
||||
Interface,
|
||||
Link,
|
||||
LinkType,
|
||||
Node,
|
||||
Session,
|
||||
ThroughputsEvent,
|
||||
)
|
||||
from core.gui.dialogs.shapemod import ShapeDialog
|
||||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import (
|
||||
|
@ -30,6 +22,7 @@ from core.gui.graph.shape import Shape
|
|||
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||
from core.gui.images import ImageEnum, TypeToImage
|
||||
from core.gui.nodeutils import NodeDraw, NodeUtils
|
||||
from core.gui.wrappers import Interface, Link, LinkType, Node, Session, ThroughputsEvent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -104,6 +97,8 @@ class CanvasGraph(tk.Canvas):
|
|||
# drawing related
|
||||
self.show_node_labels: ShowVar = ShowVar(self, tags.NODE_LABEL, value=True)
|
||||
self.show_link_labels: ShowVar = ShowVar(self, tags.LINK_LABEL, value=True)
|
||||
self.show_links: ShowVar = ShowVar(self, tags.EDGE, value=True)
|
||||
self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True)
|
||||
self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True)
|
||||
self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True)
|
||||
self.show_iface_names: BooleanVar = BooleanVar(value=False)
|
||||
|
@ -135,11 +130,6 @@ class CanvasGraph(tk.Canvas):
|
|||
self.configure(scrollregion=self.bbox(tk.ALL))
|
||||
|
||||
def reset_and_redraw(self, session: Session) -> None:
|
||||
"""
|
||||
Reset the private variables CanvasGraph object, redraw nodes given the new grpc
|
||||
client.
|
||||
:param session: session to draw
|
||||
"""
|
||||
# reset view options to default state
|
||||
self.show_node_labels.set(True)
|
||||
self.show_link_labels.set(True)
|
||||
|
@ -251,12 +241,12 @@ class CanvasGraph(tk.Canvas):
|
|||
dst.edges.add(edge)
|
||||
self.edges[edge.token] = edge
|
||||
self.core.links[edge.token] = edge
|
||||
if link.HasField("iface1"):
|
||||
if link.iface1:
|
||||
iface1 = link.iface1
|
||||
self.core.iface_to_edge[(node1.id, iface1.id)] = token
|
||||
src.ifaces[iface1.id] = iface1
|
||||
edge.src_iface = iface1
|
||||
if link.HasField("iface2"):
|
||||
if link.iface2:
|
||||
iface2 = link.iface2
|
||||
self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
|
||||
dst.ifaces[iface2.id] = iface2
|
||||
|
@ -274,7 +264,7 @@ class CanvasGraph(tk.Canvas):
|
|||
edge = self.edges.get(token)
|
||||
if not edge:
|
||||
return
|
||||
edge.link.options.CopyFrom(link.options)
|
||||
edge.link.options = deepcopy(link.options)
|
||||
|
||||
def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None:
|
||||
network_id = link.network_id if link.network_id else None
|
||||
|
@ -323,10 +313,7 @@ class CanvasGraph(tk.Canvas):
|
|||
edge.middle_label_text(link.label)
|
||||
|
||||
def add_core_node(self, core_node: Node) -> None:
|
||||
if core_node.id in self.core.canvas_nodes:
|
||||
logging.error("core node already exists: %s", core_node)
|
||||
return
|
||||
logging.debug("adding node %s", core_node)
|
||||
logging.debug("adding node: %s", core_node)
|
||||
# if the gui can't find node's image, default to the "edit-node" image
|
||||
image = NodeUtils.node_image(core_node, self.app.guiconfig, self.app.app_scale)
|
||||
if not image:
|
||||
|
@ -335,24 +322,24 @@ class CanvasGraph(tk.Canvas):
|
|||
y = core_node.position.y
|
||||
node = CanvasNode(self.app, x, y, core_node, image)
|
||||
self.nodes[node.id] = node
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
self.core.set_canvas_node(core_node, node)
|
||||
|
||||
def draw_session(self, session: Session) -> None:
|
||||
"""
|
||||
Draw existing session.
|
||||
"""
|
||||
# draw existing nodes
|
||||
for core_node in session.nodes:
|
||||
for core_node in session.nodes.values():
|
||||
logging.debug("drawing node: %s", core_node)
|
||||
# peer to peer node is not drawn on the GUI
|
||||
if NodeUtils.is_ignore_node(core_node.type):
|
||||
continue
|
||||
self.add_core_node(core_node)
|
||||
|
||||
# draw existing links
|
||||
# draw existing links
|
||||
for link in session.links:
|
||||
logging.debug("drawing link: %s", link)
|
||||
canvas_node1 = self.core.canvas_nodes[link.node1_id]
|
||||
canvas_node2 = self.core.canvas_nodes[link.node2_id]
|
||||
canvas_node1 = self.core.get_canvas_node(link.node1_id)
|
||||
canvas_node2 = self.core.get_canvas_node(link.node2_id)
|
||||
if link.type == LinkType.WIRELESS:
|
||||
self.add_wireless_edge(canvas_node1, canvas_node2, link)
|
||||
else:
|
||||
|
@ -555,8 +542,8 @@ class CanvasGraph(tk.Canvas):
|
|||
shape.delete()
|
||||
|
||||
self.selection.clear()
|
||||
self.core.deleted_graph_nodes(nodes)
|
||||
self.core.deleted_graph_edges(edges)
|
||||
self.core.deleted_canvas_nodes(nodes)
|
||||
self.core.deleted_canvas_edges(edges)
|
||||
|
||||
def delete_edge(self, edge: CanvasEdge) -> None:
|
||||
edge.delete()
|
||||
|
@ -575,7 +562,7 @@ class CanvasGraph(tk.Canvas):
|
|||
dst_wireless = NodeUtils.is_wireless_node(dst_node.core_node.type)
|
||||
if dst_wireless:
|
||||
src_node.delete_antenna()
|
||||
self.core.deleted_graph_edges([edge])
|
||||
self.core.deleted_canvas_edges([edge])
|
||||
|
||||
def zoom(self, event: tk.Event, factor: float = None) -> None:
|
||||
if not factor:
|
||||
|
@ -761,8 +748,8 @@ class CanvasGraph(tk.Canvas):
|
|||
image_file = self.node_draw.image_file
|
||||
self.node_draw.image = self.app.get_custom_icon(image_file, ICON_SIZE)
|
||||
node = CanvasNode(self.app, x, y, core_node, self.node_draw.image)
|
||||
self.core.canvas_nodes[core_node.id] = node
|
||||
self.nodes[node.id] = node
|
||||
self.core.set_canvas_node(core_node, node)
|
||||
|
||||
def width_and_height(self) -> Tuple[int, int]:
|
||||
"""
|
||||
|
@ -939,7 +926,8 @@ class CanvasGraph(tk.Canvas):
|
|||
# maps original node canvas id to copy node canvas id
|
||||
copy_map = {}
|
||||
# the edges that will be copy over
|
||||
to_copy_edges = []
|
||||
to_copy_edges = set()
|
||||
to_copy_ids = {x.id for x in self.to_copy}
|
||||
for canvas_node in self.to_copy:
|
||||
core_node = canvas_node.core_node
|
||||
actual_x = core_node.position.x + 50
|
||||
|
@ -951,30 +939,57 @@ class CanvasGraph(tk.Canvas):
|
|||
if not copy:
|
||||
continue
|
||||
node = CanvasNode(self.app, scaled_x, scaled_y, copy, canvas_node.image)
|
||||
|
||||
# copy configurations and services
|
||||
node.core_node.services[:] = canvas_node.core_node.services
|
||||
node.core_node.config_services[:] = canvas_node.core_node.config_services
|
||||
node.emane_model_configs = deepcopy(canvas_node.emane_model_configs)
|
||||
node.wlan_config = deepcopy(canvas_node.wlan_config)
|
||||
node.mobility_config = deepcopy(canvas_node.mobility_config)
|
||||
node.service_configs = deepcopy(canvas_node.service_configs)
|
||||
node.service_file_configs = deepcopy(canvas_node.service_file_configs)
|
||||
node.config_service_configs = deepcopy(canvas_node.config_service_configs)
|
||||
node.core_node.services = core_node.services.copy()
|
||||
node.core_node.config_services = core_node.config_services.copy()
|
||||
node.core_node.emane_model_configs = deepcopy(core_node.emane_model_configs)
|
||||
node.core_node.wlan_config = deepcopy(core_node.wlan_config)
|
||||
node.core_node.mobility_config = deepcopy(core_node.mobility_config)
|
||||
node.core_node.service_configs = deepcopy(core_node.service_configs)
|
||||
node.core_node.service_file_configs = deepcopy(
|
||||
core_node.service_file_configs
|
||||
)
|
||||
node.core_node.config_service_configs = deepcopy(
|
||||
core_node.config_service_configs
|
||||
)
|
||||
|
||||
copy_map[canvas_node.id] = node.id
|
||||
self.core.canvas_nodes[copy.id] = node
|
||||
self.nodes[node.id] = node
|
||||
self.core.set_canvas_node(copy, node)
|
||||
for edge in canvas_node.edges:
|
||||
if edge.src not in self.to_copy or edge.dst not in self.to_copy:
|
||||
if edge.src not in to_copy_ids or edge.dst not in to_copy_ids:
|
||||
if canvas_node.id == edge.src:
|
||||
dst_node = self.nodes[edge.dst]
|
||||
self.create_edge(node, dst_node)
|
||||
token = create_edge_token(node.id, dst_node.id)
|
||||
elif canvas_node.id == edge.dst:
|
||||
src_node = self.nodes[edge.src]
|
||||
self.create_edge(src_node, node)
|
||||
token = create_edge_token(src_node.id, node.id)
|
||||
copy_edge = self.edges[token]
|
||||
copy_link = copy_edge.link
|
||||
iface1_id = copy_link.iface1.id if copy_link.iface1 else None
|
||||
iface2_id = copy_link.iface2.id if copy_link.iface2 else None
|
||||
options = edge.link.options
|
||||
if options:
|
||||
copy_edge.link.options = deepcopy(options)
|
||||
if options and options.unidirectional:
|
||||
asym_iface1 = None
|
||||
if iface1_id is not None:
|
||||
asym_iface1 = Interface(id=iface1_id)
|
||||
asym_iface2 = None
|
||||
if iface2_id is not None:
|
||||
asym_iface2 = Interface(id=iface2_id)
|
||||
copy_edge.asymmetric_link = Link(
|
||||
node1_id=copy_link.node2_id,
|
||||
node2_id=copy_link.node1_id,
|
||||
iface1=asym_iface2,
|
||||
iface2=asym_iface1,
|
||||
options=deepcopy(edge.asymmetric_link.options),
|
||||
)
|
||||
copy_edge.redraw()
|
||||
else:
|
||||
to_copy_edges.append(edge)
|
||||
to_copy_edges.add(edge)
|
||||
|
||||
# copy link and link config
|
||||
for edge in to_copy_edges:
|
||||
|
@ -986,30 +1001,26 @@ class CanvasGraph(tk.Canvas):
|
|||
token = create_edge_token(src_node_copy.id, dst_node_copy.id)
|
||||
copy_edge = self.edges[token]
|
||||
copy_link = copy_edge.link
|
||||
iface1_id = copy_link.iface1.id if copy_link.iface1 else None
|
||||
iface2_id = copy_link.iface2.id if copy_link.iface2 else None
|
||||
options = edge.link.options
|
||||
copy_link.options.CopyFrom(options)
|
||||
iface1_id = None
|
||||
if copy_link.HasField("iface1"):
|
||||
iface1_id = copy_link.iface1.id
|
||||
iface2_id = None
|
||||
if copy_link.HasField("iface2"):
|
||||
iface2_id = copy_link.iface2.id
|
||||
if not options.unidirectional:
|
||||
copy_edge.asymmetric_link = None
|
||||
else:
|
||||
if options:
|
||||
copy_link.options = deepcopy(options)
|
||||
if options and options.unidirectional:
|
||||
asym_iface1 = None
|
||||
if iface1_id:
|
||||
if iface1_id is not None:
|
||||
asym_iface1 = Interface(id=iface1_id)
|
||||
asym_iface2 = None
|
||||
if iface2_id:
|
||||
if iface2_id is not None:
|
||||
asym_iface2 = Interface(id=iface2_id)
|
||||
copy_edge.asymmetric_link = Link(
|
||||
node1_id=copy_link.node2_id,
|
||||
node2_id=copy_link.node1_id,
|
||||
iface1=asym_iface1,
|
||||
iface2=asym_iface2,
|
||||
options=edge.asymmetric_link.options,
|
||||
iface1=asym_iface2,
|
||||
iface2=asym_iface1,
|
||||
options=deepcopy(edge.asymmetric_link.options),
|
||||
)
|
||||
copy_edge.redraw()
|
||||
self.itemconfig(
|
||||
copy_edge.id,
|
||||
width=self.itemcget(edge.id, "width"),
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import functools
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
|
||||
import grpc
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import Interface, Node, NodeType
|
||||
from core.api.grpc.services_pb2 import NodeServiceData
|
||||
from core.gui import themes
|
||||
from core.gui import nodeutils, themes
|
||||
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
|
||||
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
|
||||
from core.gui.dialogs.nodeconfig import NodeConfigDialog
|
||||
|
@ -20,8 +18,9 @@ from core.gui.frames.node import NodeInfoFrame
|
|||
from core.gui.graph import tags
|
||||
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
|
||||
from core.gui.graph.tooltip import CanvasTooltip
|
||||
from core.gui.images import ImageEnum
|
||||
from core.gui.images import ImageEnum, Images
|
||||
from core.gui.nodeutils import ANTENNA_SIZE, NodeUtils
|
||||
from core.gui.wrappers import Interface, Node, NodeType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -58,15 +57,6 @@ class CanvasNode:
|
|||
self.wireless_edges: Set[CanvasWirelessEdge] = set()
|
||||
self.antennas: List[int] = []
|
||||
self.antenna_images: Dict[int, PhotoImage] = {}
|
||||
# possible configurations
|
||||
self.emane_model_configs: Dict[
|
||||
Tuple[str, Optional[int]], Dict[str, ConfigOption]
|
||||
] = {}
|
||||
self.wlan_config: Dict[str, ConfigOption] = {}
|
||||
self.mobility_config: Dict[str, ConfigOption] = {}
|
||||
self.service_configs: Dict[str, NodeServiceData] = {}
|
||||
self.service_file_configs: Dict[str, Dict[str, str]] = {}
|
||||
self.config_service_configs: Dict[str, Any] = {}
|
||||
self.setup_bindings()
|
||||
self.context: tk.Menu = tk.Menu(self.canvas)
|
||||
themes.style_menu(self.context)
|
||||
|
@ -217,6 +207,7 @@ class CanvasNode:
|
|||
self.context.delete(0, tk.END)
|
||||
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN
|
||||
is_emane = self.core_node.type == NodeType.EMANE
|
||||
is_mobility = is_wlan or is_emane
|
||||
if self.app.core.is_runtime():
|
||||
self.context.add_command(label="Configure", command=self.show_config)
|
||||
if is_emane:
|
||||
|
@ -227,7 +218,7 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
label="WLAN Config", command=self.show_wlan_config
|
||||
)
|
||||
if is_wlan and self.core_node.id in self.app.core.mobility_players:
|
||||
if is_mobility and self.core_node.id in self.app.core.mobility_players:
|
||||
self.context.add_command(
|
||||
label="Mobility Player", command=self.show_mobility_player
|
||||
)
|
||||
|
@ -246,6 +237,7 @@ class CanvasNode:
|
|||
self.context.add_command(
|
||||
label="WLAN Config", command=self.show_wlan_config
|
||||
)
|
||||
if is_mobility:
|
||||
self.context.add_command(
|
||||
label="Mobility Config", command=self.show_mobility_config
|
||||
)
|
||||
|
@ -301,7 +293,7 @@ class CanvasNode:
|
|||
dialog.show()
|
||||
|
||||
def show_mobility_config(self) -> None:
|
||||
dialog = MobilityConfigDialog(self.app, self)
|
||||
dialog = MobilityConfigDialog(self.app, self.core_node)
|
||||
if not dialog.has_error:
|
||||
dialog.show()
|
||||
|
||||
|
@ -310,15 +302,15 @@ class CanvasNode:
|
|||
mobility_player.show()
|
||||
|
||||
def show_emane_config(self) -> None:
|
||||
dialog = EmaneConfigDialog(self.app, self)
|
||||
dialog = EmaneConfigDialog(self.app, self.core_node)
|
||||
dialog.show()
|
||||
|
||||
def show_services(self) -> None:
|
||||
dialog = NodeServiceDialog(self.app, self)
|
||||
dialog = NodeServiceDialog(self.app, self.core_node)
|
||||
dialog.show()
|
||||
|
||||
def show_config_services(self) -> None:
|
||||
dialog = NodeConfigServiceDialog(self.app, self)
|
||||
dialog = NodeConfigServiceDialog(self.app, self.core_node)
|
||||
dialog.show()
|
||||
|
||||
def has_emane_link(self, iface_id: int) -> Node:
|
||||
|
@ -356,3 +348,11 @@ class CanvasNode:
|
|||
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
|
||||
dy = node_y - int(23 * self.app.app_scale) - y
|
||||
self.canvas.move(antenna_id, dx, dy)
|
||||
|
||||
def update_icon(self, icon_path: str) -> None:
|
||||
if not Path(icon_path).exists():
|
||||
logging.error(f"node icon does not exist: {icon_path}")
|
||||
return
|
||||
self.core_node.icon = icon_path
|
||||
self.image = Images.create(icon_path, nodeutils.ICON_SIZE)
|
||||
self.canvas.itemconfig(self.id, image=self.image)
|
||||
|
|
|
@ -5,8 +5,8 @@ from typing import Dict, Optional, Tuple
|
|||
from PIL import Image
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.core_pb2 import NodeType
|
||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||
from core.gui.wrappers import NodeType
|
||||
|
||||
|
||||
class Images:
|
||||
|
|
|
@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
|||
import netaddr
|
||||
from netaddr import EUI, IPNetwork
|
||||
|
||||
from core.api.grpc.core_pb2 import Interface, Link, Node
|
||||
from core.gui.graph.node import CanvasNode
|
||||
from core.gui.nodeutils import NodeUtils
|
||||
from core.gui.wrappers import Interface, Link, Node
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -89,10 +89,10 @@ class InterfaceManager:
|
|||
remaining_subnets = set()
|
||||
for edge in self.app.core.links.values():
|
||||
link = edge.link
|
||||
if link.HasField("iface1"):
|
||||
if link.iface1:
|
||||
subnets = self.get_subnets(link.iface1)
|
||||
remaining_subnets.add(subnets)
|
||||
if link.HasField("iface2"):
|
||||
if link.iface2:
|
||||
subnets = self.get_subnets(link.iface2)
|
||||
remaining_subnets.add(subnets)
|
||||
|
||||
|
@ -100,9 +100,9 @@ class InterfaceManager:
|
|||
# or remove used indexes from subnet
|
||||
ifaces = []
|
||||
for link in links:
|
||||
if link.HasField("iface1"):
|
||||
if link.iface1:
|
||||
ifaces.append(link.iface1)
|
||||
if link.HasField("iface2"):
|
||||
if link.iface2:
|
||||
ifaces.append(link.iface2)
|
||||
for iface in ifaces:
|
||||
subnets = self.get_subnets(iface)
|
||||
|
@ -117,9 +117,9 @@ class InterfaceManager:
|
|||
def joined(self, links: List[Link]) -> None:
|
||||
ifaces = []
|
||||
for link in links:
|
||||
if link.HasField("iface1"):
|
||||
if link.iface1:
|
||||
ifaces.append(link.iface1)
|
||||
if link.HasField("iface2"):
|
||||
if link.iface2:
|
||||
ifaces.append(link.iface2)
|
||||
|
||||
# add to used subnets and mark used indexes
|
||||
|
|
|
@ -6,7 +6,6 @@ from functools import partial
|
|||
from tkinter import filedialog, messagebox
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from core.gui.appconfig import XMLS_PATH
|
||||
from core.gui.coreclient import CoreClient
|
||||
from core.gui.dialogs.about import AboutDialog
|
||||
from core.gui.dialogs.canvassizeandscale import SizeAndScaleDialog
|
||||
|
@ -168,6 +167,16 @@ class Menubar(tk.Menu):
|
|||
command=self.canvas.show_link_labels.click_handler,
|
||||
variable=self.canvas.show_link_labels,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Links",
|
||||
command=self.canvas.show_links.click_handler,
|
||||
variable=self.canvas.show_links,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Wireless Links",
|
||||
command=self.canvas.show_wireless.click_handler,
|
||||
variable=self.canvas.show_wireless,
|
||||
)
|
||||
menu.add_checkbutton(
|
||||
label="Annotations",
|
||||
command=self.canvas.show_annotations.click_handler,
|
||||
|
@ -265,16 +274,13 @@ class Menubar(tk.Menu):
|
|||
)
|
||||
|
||||
def click_save(self, _event=None) -> None:
|
||||
xml_file = self.core.xml_file
|
||||
if xml_file:
|
||||
self.core.save_xml(xml_file)
|
||||
if self.core.session.file:
|
||||
self.core.save_xml()
|
||||
else:
|
||||
self.click_save_xml()
|
||||
|
||||
def click_save_xml(self, _event: tk.Event = None) -> None:
|
||||
init_dir = self.core.xml_dir
|
||||
if not init_dir:
|
||||
init_dir = str(XMLS_PATH)
|
||||
init_dir = self.core.get_xml_dir()
|
||||
file_path = filedialog.asksaveasfilename(
|
||||
initialdir=init_dir,
|
||||
title="Save As",
|
||||
|
@ -284,12 +290,9 @@ class Menubar(tk.Menu):
|
|||
if file_path:
|
||||
self.add_recent_file_to_gui_config(file_path)
|
||||
self.core.save_xml(file_path)
|
||||
self.core.xml_file = file_path
|
||||
|
||||
def click_open_xml(self, _event: tk.Event = None) -> None:
|
||||
init_dir = self.core.xml_dir
|
||||
if not init_dir:
|
||||
init_dir = str(XMLS_PATH)
|
||||
init_dir = self.core.get_xml_dir()
|
||||
file_path = filedialog.askopenfilename(
|
||||
initialdir=init_dir,
|
||||
title="Open",
|
||||
|
@ -298,12 +301,10 @@ class Menubar(tk.Menu):
|
|||
if file_path:
|
||||
self.open_xml_task(file_path)
|
||||
|
||||
def open_xml_task(self, filename: str) -> None:
|
||||
self.add_recent_file_to_gui_config(filename)
|
||||
self.core.xml_file = filename
|
||||
self.core.xml_dir = str(os.path.dirname(filename))
|
||||
def open_xml_task(self, file_path: str) -> None:
|
||||
self.add_recent_file_to_gui_config(file_path)
|
||||
self.prompt_save_running_session()
|
||||
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(filename,))
|
||||
task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,))
|
||||
task.start()
|
||||
|
||||
def execute_python(self) -> None:
|
||||
|
@ -357,7 +358,6 @@ class Menubar(tk.Menu):
|
|||
def click_new(self) -> None:
|
||||
self.prompt_save_running_session()
|
||||
self.core.create_new_session()
|
||||
self.core.xml_file = None
|
||||
|
||||
def click_find(self, _event: tk.Event = None) -> None:
|
||||
dialog = FindDialog(self.app)
|
||||
|
|
|
@ -3,9 +3,9 @@ from typing import List, Optional, Set
|
|||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc.core_pb2 import Node, NodeType
|
||||
from core.gui.appconfig import CustomNode, GuiConfig
|
||||
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||
from core.gui.wrappers import Node, NodeType
|
||||
|
||||
ICON_SIZE: int = 48
|
||||
ANTENNA_SIZE: int = 32
|
||||
|
@ -17,7 +17,7 @@ class NodeDraw:
|
|||
self.image: Optional[PhotoImage] = None
|
||||
self.image_enum: Optional[ImageEnum] = None
|
||||
self.image_file: Optional[str] = None
|
||||
self.node_type: NodeType = None
|
||||
self.node_type: Optional[NodeType] = None
|
||||
self.model: Optional[str] = None
|
||||
self.services: Set[str] = set()
|
||||
self.label: Optional[str] = None
|
||||
|
@ -63,10 +63,15 @@ class NodeUtils:
|
|||
WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
RJ45_NODES: Set[NodeType] = {NodeType.RJ45}
|
||||
IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET}
|
||||
MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE}
|
||||
NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"}
|
||||
ROUTER_NODES: Set[str] = {"router", "mdr"}
|
||||
ANTENNA_ICON: PhotoImage = None
|
||||
|
||||
@classmethod
|
||||
def is_mobility(cls, node: Node) -> bool:
|
||||
return node.type in cls.MOBILITY_NODES
|
||||
|
||||
@classmethod
|
||||
def is_router_node(cls, node: Node) -> bool:
|
||||
return cls.is_model_node(node.type) and node.model in cls.ROUTER_NODES
|
||||
|
|
|
@ -5,9 +5,9 @@ import tkinter as tk
|
|||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from core.api.grpc.core_pb2 import ExceptionEvent, ExceptionLevel
|
||||
from core.gui.dialogs.alerts import AlertsDialog
|
||||
from core.gui.themes import Styles
|
||||
from core.gui.wrappers import ExceptionEvent, ExceptionLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -34,7 +34,7 @@ class StatusBar(ttk.Frame):
|
|||
self.columnconfigure(3, weight=1)
|
||||
|
||||
frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE)
|
||||
frame.grid(row=0, column=0, sticky="ew")
|
||||
frame.grid(row=0, column=0, sticky=tk.EW)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.status = ttk.Label(
|
||||
|
@ -44,22 +44,22 @@ class StatusBar(ttk.Frame):
|
|||
borderwidth=1,
|
||||
relief=tk.RIDGE,
|
||||
)
|
||||
self.status.grid(row=0, column=0, sticky="ew")
|
||||
self.status.grid(row=0, column=0, sticky=tk.EW)
|
||||
|
||||
self.zoom = ttk.Label(self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE)
|
||||
self.zoom.grid(row=0, column=1, sticky="ew")
|
||||
self.zoom.grid(row=0, column=1, sticky=tk.EW)
|
||||
self.set_zoom(self.app.canvas.ratio)
|
||||
|
||||
self.cpu_label = ttk.Label(
|
||||
self, anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE
|
||||
)
|
||||
self.cpu_label.grid(row=0, column=2, sticky="ew")
|
||||
self.cpu_label.grid(row=0, column=2, sticky=tk.EW)
|
||||
self.set_cpu(0.0)
|
||||
|
||||
self.alerts_button = ttk.Button(
|
||||
self, text="Alerts", command=self.click_alerts, style=self.alert_style
|
||||
)
|
||||
self.alerts_button.grid(row=0, column=3, sticky="ew")
|
||||
self.alerts_button.grid(row=0, column=3, sticky=tk.EW)
|
||||
|
||||
def set_cpu(self, usage: float) -> None:
|
||||
self.cpu_label.config(text=f"CPU {usage * 100:.2f}%")
|
||||
|
@ -69,7 +69,7 @@ class StatusBar(ttk.Frame):
|
|||
|
||||
def add_alert(self, event: ExceptionEvent) -> None:
|
||||
self.core_alarms.append(event)
|
||||
level = event.exception_event.level
|
||||
level = event.level
|
||||
self._set_alert_style(level)
|
||||
label = f"Alerts ({len(self.core_alarms)})"
|
||||
self.alerts_button.config(text=label, style=self.alert_style)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -26,22 +27,20 @@ class ProgressTask:
|
|||
self.time: Optional[float] = None
|
||||
|
||||
def start(self) -> None:
|
||||
self.app.progress.grid(sticky="ew", columnspan=2)
|
||||
self.app.progress.grid(sticky=tk.EW, columnspan=2)
|
||||
self.app.progress.start()
|
||||
self.time = time.perf_counter()
|
||||
thread = threading.Thread(target=self.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def run(self) -> None:
|
||||
logging.info("running task")
|
||||
try:
|
||||
values = self.task(*self.args)
|
||||
if values is None:
|
||||
values = ()
|
||||
elif values and not isinstance(values, tuple):
|
||||
elif values is not None and not isinstance(values, tuple):
|
||||
values = (values,)
|
||||
if self.callback:
|
||||
logging.info("calling callback")
|
||||
self.app.after(0, self.callback, *values)
|
||||
except Exception as e:
|
||||
logging.exception("progress task exception")
|
||||
|
|
|
@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Callable, List, Optional
|
|||
|
||||
from PIL.ImageTk import PhotoImage
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.colorpicker import ColorPickerDialog
|
||||
from core.gui.dialogs.runtool import RunToolDialog
|
||||
from core.gui.graph import tags
|
||||
|
@ -96,7 +95,7 @@ class ButtonBar(ttk.Frame):
|
|||
image = self.app.get_icon(image_enum, TOOLBAR_SIZE)
|
||||
button = ttk.Button(self, image=image, command=func)
|
||||
button.image = image
|
||||
button.grid(sticky="ew")
|
||||
button.grid(sticky=tk.EW)
|
||||
Tooltip(button, tooltip)
|
||||
if radio:
|
||||
self.radio_buttons.append(button)
|
||||
|
@ -125,7 +124,7 @@ class MarkerFrame(ttk.Frame):
|
|||
image = self.app.get_icon(ImageEnum.DELETE, 16)
|
||||
button = ttk.Button(self, image=image, width=2, command=self.click_clear)
|
||||
button.image = image
|
||||
button.grid(sticky="ew", pady=self.PAD)
|
||||
button.grid(sticky=tk.EW, pady=self.PAD)
|
||||
Tooltip(button, "Delete Marker")
|
||||
|
||||
sizes = [1, 3, 8, 10]
|
||||
|
@ -133,14 +132,14 @@ class MarkerFrame(ttk.Frame):
|
|||
sizes = ttk.Combobox(
|
||||
self, state="readonly", textvariable=self.size, value=sizes, width=2
|
||||
)
|
||||
sizes.grid(sticky="ew", pady=self.PAD)
|
||||
sizes.grid(sticky=tk.EW, pady=self.PAD)
|
||||
Tooltip(sizes, "Marker Size")
|
||||
|
||||
frame_size = TOOLBAR_SIZE
|
||||
self.color_frame = tk.Frame(
|
||||
self, background=self.color, height=frame_size, width=frame_size
|
||||
)
|
||||
self.color_frame.grid(sticky="ew")
|
||||
self.color_frame.grid(sticky=tk.EW)
|
||||
self.color_frame.bind("<Button-1>", self.click_color)
|
||||
Tooltip(self.color_frame, "Marker Color")
|
||||
|
||||
|
@ -208,7 +207,7 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def draw_design_frame(self) -> None:
|
||||
self.design_frame = ButtonBar(self, self.app)
|
||||
self.design_frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.design_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.design_frame.columnconfigure(0, weight=1)
|
||||
self.play_button = self.design_frame.create_button(
|
||||
ImageEnum.START, self.click_start, "Start Session"
|
||||
|
@ -240,7 +239,7 @@ class Toolbar(ttk.Frame):
|
|||
|
||||
def draw_runtime_frame(self) -> None:
|
||||
self.runtime_frame = ButtonBar(self, self.app)
|
||||
self.runtime_frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.runtime_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.runtime_frame.columnconfigure(0, weight=1)
|
||||
self.stop_button = self.runtime_frame.create_button(
|
||||
ImageEnum.STOP, self.click_stop, "Stop Session"
|
||||
|
@ -300,15 +299,15 @@ class Toolbar(ttk.Frame):
|
|||
)
|
||||
task.start()
|
||||
|
||||
def start_callback(self, response: core_pb2.StartSessionResponse) -> None:
|
||||
if response.result:
|
||||
def start_callback(self, result: bool, exceptions: List[str]) -> None:
|
||||
if result:
|
||||
self.set_runtime()
|
||||
self.app.core.set_metadata()
|
||||
self.app.core.show_mobility_players()
|
||||
else:
|
||||
enable_buttons(self.design_frame, enabled=True)
|
||||
if response.exceptions:
|
||||
message = "\n".join(response.exceptions)
|
||||
if exceptions:
|
||||
message = "\n".join(exceptions)
|
||||
self.app.show_error("Start Session Error", message)
|
||||
|
||||
def set_runtime(self) -> None:
|
||||
|
@ -388,7 +387,7 @@ class Toolbar(ttk.Frame):
|
|||
self.runtime_frame, image=image, direction=tk.RIGHT
|
||||
)
|
||||
menu_button.image = image
|
||||
menu_button.grid(sticky="ew")
|
||||
menu_button.grid(sticky=tk.EW)
|
||||
self.observers_menu = ObserversMenu(menu_button, self.app)
|
||||
menu_button["menu"] = self.observers_menu
|
||||
|
||||
|
@ -405,7 +404,7 @@ class Toolbar(ttk.Frame):
|
|||
)
|
||||
task.start()
|
||||
|
||||
def stop_callback(self, response: core_pb2.StopSessionResponse) -> None:
|
||||
def stop_callback(self, result: bool) -> None:
|
||||
self.set_design()
|
||||
self.app.canvas.stopped_session()
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class Tooltip(object):
|
|||
self.tw.rowconfigure(0, weight=1)
|
||||
self.tw.columnconfigure(0, weight=1)
|
||||
frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3)
|
||||
frame.grid(sticky="nsew")
|
||||
frame.grid(sticky=tk.NSEW)
|
||||
label = ttk.Label(frame, text=self.text, style=Styles.tooltip)
|
||||
label.grid()
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
|
||||
def bandwidth_text(bandwidth: int) -> str:
|
||||
size = {0: "bps", 1: "Kbps", 2: "Mbps", 3: "Gbps"}
|
||||
unit = 1000
|
||||
|
@ -8,3 +11,12 @@ def bandwidth_text(bandwidth: int) -> str:
|
|||
if i == 3:
|
||||
break
|
||||
return f"{bandwidth} {size[i]}"
|
||||
|
||||
|
||||
def delay_jitter_text(delay: int, jitter: int) -> Optional[str]:
|
||||
line = None
|
||||
if delay > 0 and jitter > 0:
|
||||
line = f"{delay} us (\u00B1{jitter} us)"
|
||||
elif jitter > 0:
|
||||
line = f"0 us (\u00B1{jitter} us)"
|
||||
return line
|
||||
|
|
|
@ -5,12 +5,10 @@ from pathlib import Path
|
|||
from tkinter import filedialog, font, ttk
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Type
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc.common_pb2 import ConfigOption
|
||||
from core.api.grpc.core_pb2 import ConfigOptionType
|
||||
from core.gui import themes, validation
|
||||
from core.gui import appconfig, themes, validation
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.wrappers import ConfigOption, ConfigOptionType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
@ -28,7 +26,9 @@ INT_TYPES: Set[ConfigOptionType] = {
|
|||
|
||||
|
||||
def file_button_click(value: tk.StringVar, parent: tk.Widget) -> None:
|
||||
file_path = filedialog.askopenfilename(title="Select File", parent=parent)
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select File", initialdir=str(appconfig.HOME_PATH), parent=parent
|
||||
)
|
||||
if file_path:
|
||||
value.set(file_path)
|
||||
|
||||
|
@ -47,13 +47,13 @@ class FrameScroll(ttk.Frame):
|
|||
self.columnconfigure(0, weight=1)
|
||||
bg = self.app.style.lookup(".", "background")
|
||||
self.canvas: tk.Canvas = tk.Canvas(self, highlightthickness=0, background=bg)
|
||||
self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2)
|
||||
self.canvas.grid(row=0, sticky=tk.NSEW, padx=2, pady=2)
|
||||
self.canvas.columnconfigure(0, weight=1)
|
||||
self.canvas.rowconfigure(0, weight=1)
|
||||
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(
|
||||
self, orient="vertical", command=self.canvas.yview
|
||||
)
|
||||
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.scrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.frame: ttk.Frame = _cls(self.canvas)
|
||||
self.frame_id: int = self.canvas.create_window(
|
||||
0, 0, anchor="nw", window=self.frame
|
||||
|
@ -108,15 +108,15 @@ class ConfigFrame(ttk.Notebook):
|
|||
self.add(tab, text=group_name)
|
||||
for index, option in enumerate(sorted(group, key=lambda x: x.name)):
|
||||
label = ttk.Label(tab.frame, text=option.label)
|
||||
label.grid(row=index, pady=PADY, padx=PADX, sticky="w")
|
||||
label.grid(row=index, pady=PADY, padx=PADX, sticky=tk.W)
|
||||
value = tk.StringVar()
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
if option.type == ConfigOptionType.BOOL:
|
||||
select = ("On", "Off")
|
||||
state = "readonly" if self.enabled else tk.DISABLED
|
||||
combobox = ttk.Combobox(
|
||||
tab.frame, textvariable=value, values=select, state=state
|
||||
)
|
||||
combobox.grid(row=index, column=1, sticky="ew")
|
||||
combobox.grid(row=index, column=1, sticky=tk.EW)
|
||||
if option.value == "1":
|
||||
value.set("On")
|
||||
else:
|
||||
|
@ -128,16 +128,16 @@ class ConfigFrame(ttk.Notebook):
|
|||
combobox = ttk.Combobox(
|
||||
tab.frame, textvariable=value, values=select, state=state
|
||||
)
|
||||
combobox.grid(row=index, column=1, sticky="ew")
|
||||
elif option.type == core_pb2.ConfigOptionType.STRING:
|
||||
combobox.grid(row=index, column=1, sticky=tk.EW)
|
||||
elif option.type == ConfigOptionType.STRING:
|
||||
value.set(option.value)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
if "file" in option.label:
|
||||
file_frame = ttk.Frame(tab.frame)
|
||||
file_frame.grid(row=index, column=1, sticky="ew")
|
||||
file_frame.grid(row=index, column=1, sticky=tk.EW)
|
||||
file_frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(file_frame, textvariable=value, state=state)
|
||||
entry.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
entry.grid(row=0, column=0, sticky=tk.EW, padx=PADX)
|
||||
func = partial(file_button_click, value, self)
|
||||
button = ttk.Button(
|
||||
file_frame, text="...", command=func, state=state
|
||||
|
@ -145,21 +145,21 @@ class ConfigFrame(ttk.Notebook):
|
|||
button.grid(row=0, column=1)
|
||||
else:
|
||||
entry = ttk.Entry(tab.frame, textvariable=value, state=state)
|
||||
entry.grid(row=index, column=1, sticky="ew")
|
||||
entry.grid(row=index, column=1, sticky=tk.EW)
|
||||
elif option.type in INT_TYPES:
|
||||
value.set(option.value)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
entry = validation.PositiveIntEntry(
|
||||
tab.frame, textvariable=value, state=state
|
||||
)
|
||||
entry.grid(row=index, column=1, sticky="ew")
|
||||
elif option.type == core_pb2.ConfigOptionType.FLOAT:
|
||||
entry.grid(row=index, column=1, sticky=tk.EW)
|
||||
elif option.type == ConfigOptionType.FLOAT:
|
||||
value.set(option.value)
|
||||
state = tk.NORMAL if self.enabled else tk.DISABLED
|
||||
entry = validation.PositiveFloatEntry(
|
||||
tab.frame, textvariable=value, state=state
|
||||
)
|
||||
entry.grid(row=index, column=1, sticky="ew")
|
||||
entry.grid(row=index, column=1, sticky=tk.EW)
|
||||
else:
|
||||
logging.error("unhandled config option type: %s", option.type)
|
||||
self.values[option.name] = value
|
||||
|
@ -169,7 +169,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
option = self.config[key]
|
||||
value = self.values[key]
|
||||
config_value = value.get()
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
if option.type == ConfigOptionType.BOOL:
|
||||
if config_value == "On":
|
||||
option.value = "1"
|
||||
else:
|
||||
|
@ -182,7 +182,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
for name, data in config.items():
|
||||
option = self.config[name]
|
||||
value = self.values[name]
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
if option.type == ConfigOptionType.BOOL:
|
||||
if data == "1":
|
||||
data = "On"
|
||||
else:
|
||||
|
@ -196,7 +196,7 @@ class ListboxScroll(ttk.Frame):
|
|||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(0, weight=1)
|
||||
self.scrollbar: ttk.Scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
|
||||
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.scrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.listbox: tk.Listbox = tk.Listbox(
|
||||
self,
|
||||
selectmode=tk.BROWSE,
|
||||
|
@ -204,7 +204,7 @@ class ListboxScroll(ttk.Frame):
|
|||
exportselection=False,
|
||||
)
|
||||
themes.style_listbox(self.listbox)
|
||||
self.listbox.grid(row=0, column=0, sticky="nsew")
|
||||
self.listbox.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.scrollbar.config(command=self.listbox.yview)
|
||||
|
||||
|
||||
|
@ -224,7 +224,7 @@ class CheckboxList(FrameScroll):
|
|||
var = tk.BooleanVar(value=checked)
|
||||
func = partial(self.clicked, name, var)
|
||||
checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
|
||||
checkbox.grid(sticky="w")
|
||||
checkbox.grid(sticky=tk.W)
|
||||
|
||||
|
||||
class CodeFont(font.Font):
|
||||
|
@ -250,9 +250,9 @@ class CodeText(ttk.Frame):
|
|||
selectforeground="black",
|
||||
relief=tk.FLAT,
|
||||
)
|
||||
self.text.grid(row=0, column=0, sticky="nsew")
|
||||
self.text.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
|
||||
yscrollbar.grid(row=0, column=1, sticky="ns")
|
||||
yscrollbar.grid(row=0, column=1, sticky=tk.NS)
|
||||
self.text.configure(yscrollcommand=yscrollbar.set)
|
||||
|
||||
|
||||
|
|
662
daemon/core/gui/wrappers.py
Normal file
662
daemon/core/gui/wrappers.py
Normal 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),
|
||||
)
|
|
@ -9,10 +9,11 @@ import threading
|
|||
import time
|
||||
from functools import total_ordering
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from core import utils
|
||||
from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.emulator.data import EventData, LinkData, LinkOptions
|
||||
from core.emulator.enumerations import (
|
||||
ConfigDataTypes,
|
||||
|
@ -31,6 +32,13 @@ if TYPE_CHECKING:
|
|||
from core.emulator.session import Session
|
||||
|
||||
|
||||
def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]:
|
||||
try:
|
||||
return session.get_node(node_id, WlanNode)
|
||||
except CoreError:
|
||||
return session.get_node(node_id, EmaneNet)
|
||||
|
||||
|
||||
class MobilityManager(ModelManager):
|
||||
"""
|
||||
Member of session class for handling configuration data for mobility and
|
||||
|
@ -69,35 +77,30 @@ class MobilityManager(ModelManager):
|
|||
"""
|
||||
if node_ids is None:
|
||||
node_ids = self.nodes()
|
||||
|
||||
for node_id in node_ids:
|
||||
logging.debug("checking mobility startup for node: %s", node_id)
|
||||
logging.debug(
|
||||
"node mobility configurations: %s", self.get_all_configs(node_id)
|
||||
"node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id)
|
||||
)
|
||||
|
||||
try:
|
||||
node = self.session.get_node(node_id, WlanNode)
|
||||
node = get_mobility_node(self.session, node_id)
|
||||
# TODO: may be an issue if there are multiple mobility models
|
||||
for model in self.models.values():
|
||||
config = self.get_configs(node_id, model.name)
|
||||
if not config:
|
||||
continue
|
||||
self.set_model(node, model, config)
|
||||
if node.mobility:
|
||||
self.session.event_loop.add_event(0.0, node.mobility.startup)
|
||||
except CoreError:
|
||||
logging.exception("mobility startup error")
|
||||
logging.warning(
|
||||
"skipping mobility configuration for unknown node: %s", node_id
|
||||
)
|
||||
continue
|
||||
|
||||
for model_name in self.models:
|
||||
config = self.get_configs(node_id, model_name)
|
||||
if not config:
|
||||
continue
|
||||
model_class = self.models[model_name]
|
||||
self.set_model(node, model_class, config)
|
||||
|
||||
if node.mobility:
|
||||
self.session.event_loop.add_event(0.0, node.mobility.startup)
|
||||
|
||||
def handleevent(self, event_data: EventData) -> None:
|
||||
"""
|
||||
Handle an Event Message used to start, stop, or pause
|
||||
mobility scripts for a given WlanNode.
|
||||
mobility scripts for a given mobility network.
|
||||
|
||||
:param event_data: event data to handle
|
||||
:return: nothing
|
||||
|
@ -106,40 +109,35 @@ class MobilityManager(ModelManager):
|
|||
node_id = event_data.node
|
||||
name = event_data.name
|
||||
try:
|
||||
node = self.session.get_node(node_id, WlanNode)
|
||||
node = get_mobility_node(self.session, node_id)
|
||||
except CoreError:
|
||||
logging.exception(
|
||||
"Ignoring event for model '%s', unknown node '%s'", name, node_id
|
||||
"ignoring event for model(%s), unknown node(%s)", name, node_id
|
||||
)
|
||||
return
|
||||
|
||||
# name is e.g. "mobility:ns2script"
|
||||
models = name[9:].split(",")
|
||||
for model in models:
|
||||
try:
|
||||
cls = self.models[model]
|
||||
except KeyError:
|
||||
logging.warning("Ignoring event for unknown model '%s'", model)
|
||||
cls = self.models.get(model)
|
||||
if not cls:
|
||||
logging.warning("ignoring event for unknown model '%s'", model)
|
||||
continue
|
||||
|
||||
if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]:
|
||||
model = node.mobility
|
||||
else:
|
||||
continue
|
||||
|
||||
if model is None:
|
||||
logging.warning("Ignoring event, %s has no model", node.name)
|
||||
logging.warning("ignoring event, %s has no model", node.name)
|
||||
continue
|
||||
|
||||
if cls.name != model.name:
|
||||
logging.warning(
|
||||
"Ignoring event for %s wrong model %s,%s",
|
||||
"ignoring event for %s wrong model %s,%s",
|
||||
node.name,
|
||||
cls.name,
|
||||
model.name,
|
||||
)
|
||||
continue
|
||||
|
||||
if event_type in [EventTypes.STOP, EventTypes.RESTART]:
|
||||
model.stop(move_initial=True)
|
||||
if event_type in [EventTypes.START, EventTypes.RESTART]:
|
||||
|
@ -162,11 +160,9 @@ class MobilityManager(ModelManager):
|
|||
event_type = EventTypes.START
|
||||
elif model.state == model.STATE_PAUSED:
|
||||
event_type = EventTypes.PAUSE
|
||||
|
||||
start_time = int(model.lasttime - model.timezero)
|
||||
end_time = int(model.endtime)
|
||||
data = f"start={start_time} end={end_time}"
|
||||
|
||||
event_data = EventData(
|
||||
node=model.id,
|
||||
event_type=event_type,
|
||||
|
@ -174,15 +170,14 @@ class MobilityManager(ModelManager):
|
|||
data=data,
|
||||
time=str(time.monotonic()),
|
||||
)
|
||||
|
||||
self.session.broadcast_event(event_data)
|
||||
|
||||
def updatewlans(
|
||||
def update_nets(
|
||||
self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]
|
||||
) -> None:
|
||||
"""
|
||||
A mobility script has caused nodes in the 'moved' list to move.
|
||||
Update every WlanNode. This saves range calculations if the model
|
||||
Update every mobility network. This saves range calculations if the model
|
||||
were to recalculate for each individual node movement.
|
||||
|
||||
:param moved: moved nodes
|
||||
|
@ -191,11 +186,11 @@ class MobilityManager(ModelManager):
|
|||
"""
|
||||
for node_id in self.nodes():
|
||||
try:
|
||||
node = self.session.get_node(node_id, WlanNode)
|
||||
node = get_mobility_node(self.session, node_id)
|
||||
if node.model:
|
||||
node.model.update(moved, moved_ifaces)
|
||||
except CoreError:
|
||||
continue
|
||||
if node.model:
|
||||
node.model.update(moved, moved_ifaces)
|
||||
logging.exception("error updating mobility node")
|
||||
|
||||
|
||||
class WirelessModel(ConfigurableOptions):
|
||||
|
@ -593,7 +588,7 @@ class WayPointMobility(WirelessModel):
|
|||
self.lasttime: Optional[float] = None
|
||||
self.endtime: Optional[int] = None
|
||||
self.timezero: float = 0.0
|
||||
self.wlan: WlanNode = session.get_node(_id, WlanNode)
|
||||
self.net: Union[WlanNode, EmaneNet] = get_mobility_node(self.session, self.id)
|
||||
# these are really set in child class via confmatrix
|
||||
self.loop: bool = False
|
||||
self.refresh_ms: int = 50
|
||||
|
@ -601,6 +596,9 @@ class WayPointMobility(WirelessModel):
|
|||
# (ns-3 sets this to False as new waypoints may be added from trace)
|
||||
self.empty_queue_stop: bool = True
|
||||
|
||||
def startup(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def runround(self) -> None:
|
||||
"""
|
||||
Advance script time and move nodes.
|
||||
|
@ -643,14 +641,14 @@ class WayPointMobility(WirelessModel):
|
|||
# only move interfaces attached to self.wlan, or all nodenum in script?
|
||||
moved = []
|
||||
moved_ifaces = []
|
||||
for iface in self.wlan.get_ifaces():
|
||||
for iface in self.net.get_ifaces():
|
||||
node = iface.node
|
||||
if self.movenode(node, dt):
|
||||
moved.append(node)
|
||||
moved_ifaces.append(iface)
|
||||
|
||||
# calculate all ranges after moving nodes; this saves calculations
|
||||
self.session.mobility.updatewlans(moved, moved_ifaces)
|
||||
self.session.mobility.update_nets(moved, moved_ifaces)
|
||||
|
||||
# TODO: check session state
|
||||
self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
|
||||
|
@ -723,7 +721,7 @@ class WayPointMobility(WirelessModel):
|
|||
"""
|
||||
moved = []
|
||||
moved_ifaces = []
|
||||
for iface in self.wlan.get_ifaces():
|
||||
for iface in self.net.get_ifaces():
|
||||
node = iface.node
|
||||
if node.id not in self.initial:
|
||||
continue
|
||||
|
@ -731,7 +729,7 @@ class WayPointMobility(WirelessModel):
|
|||
self.setnodeposition(node, x, y, z)
|
||||
moved.append(node)
|
||||
moved_ifaces.append(iface)
|
||||
self.session.mobility.updatewlans(moved, moved_ifaces)
|
||||
self.session.mobility.update_nets(moved, moved_ifaces)
|
||||
|
||||
def addwaypoint(
|
||||
self,
|
||||
|
@ -1094,7 +1092,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
:return: nothing
|
||||
"""
|
||||
if self.autostart == "":
|
||||
logging.info("not auto-starting ns-2 script for %s", self.wlan.name)
|
||||
logging.info("not auto-starting ns-2 script for %s", self.net.name)
|
||||
return
|
||||
try:
|
||||
t = float(self.autostart)
|
||||
|
@ -1102,11 +1100,11 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
logging.exception(
|
||||
"Invalid auto-start seconds specified '%s' for %s",
|
||||
self.autostart,
|
||||
self.wlan.name,
|
||||
self.net.name,
|
||||
)
|
||||
return
|
||||
self.movenodesinitial()
|
||||
logging.info("scheduling ns-2 script for %s autostart at %s", self.wlan.name, t)
|
||||
logging.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t)
|
||||
self.state = self.STATE_RUNNING
|
||||
self.session.event_loop.add_event(t, self.run)
|
||||
|
||||
|
|
|
@ -10,7 +10,17 @@ services.
|
|||
import enum
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Type
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from core import utils
|
||||
from core.emulator.data import FileData
|
||||
|
@ -21,6 +31,8 @@ from core.nodes.base import CoreNode
|
|||
if TYPE_CHECKING:
|
||||
from core.emulator.session import Session
|
||||
|
||||
CoreServiceType = Union["CoreService", Type["CoreService"]]
|
||||
|
||||
|
||||
class ServiceBootError(Exception):
|
||||
pass
|
||||
|
@ -39,95 +51,56 @@ class ServiceDependencies:
|
|||
provided.
|
||||
"""
|
||||
|
||||
def __init__(self, services: List["CoreService"]) -> None:
|
||||
# helpers to check validity
|
||||
self.dependents: Dict[str, Set[str]] = {}
|
||||
self.booted: Set[str] = set()
|
||||
self.node_services: Dict[str, "CoreService"] = {}
|
||||
for service in services:
|
||||
self.node_services[service.name] = service
|
||||
for dependency in service.dependencies:
|
||||
dependents = self.dependents.setdefault(dependency, set())
|
||||
dependents.add(service.name)
|
||||
|
||||
# used to find paths
|
||||
self.path: List["CoreService"] = []
|
||||
def __init__(self, services: List["CoreServiceType"]) -> None:
|
||||
self.visited: Set[str] = set()
|
||||
self.visiting: Set[str] = set()
|
||||
self.services: Dict[str, "CoreServiceType"] = {}
|
||||
self.paths: Dict[str, List["CoreServiceType"]] = {}
|
||||
self.boot_paths: List[List["CoreServiceType"]] = []
|
||||
roots = set([x.name for x in services])
|
||||
for service in services:
|
||||
self.services[service.name] = service
|
||||
roots -= set(service.dependencies)
|
||||
self.roots: List["CoreServiceType"] = [x for x in services if x.name in roots]
|
||||
if services and not self.roots:
|
||||
raise ValueError("circular dependency is present")
|
||||
|
||||
def boot_paths(self) -> List[List["CoreService"]]:
|
||||
"""
|
||||
Generates the boot paths for the services provided to the class.
|
||||
def _search(
|
||||
self,
|
||||
service: "CoreServiceType",
|
||||
visiting: Set[str] = None,
|
||||
path: List[str] = None,
|
||||
) -> List["CoreServiceType"]:
|
||||
if service.name in self.visited:
|
||||
return self.paths[service.name]
|
||||
self.visited.add(service.name)
|
||||
if visiting is None:
|
||||
visiting = set()
|
||||
visiting.add(service.name)
|
||||
if path is None:
|
||||
for dependency in service.dependencies:
|
||||
path = self.paths.get(dependency)
|
||||
if path is not None:
|
||||
break
|
||||
for dependency in service.dependencies:
|
||||
service_dependency = self.services.get(dependency)
|
||||
if not service_dependency:
|
||||
raise ValueError(f"required dependency was not provided: {dependency}")
|
||||
if dependency in visiting:
|
||||
raise ValueError(f"circular dependency, already visited: {dependency}")
|
||||
else:
|
||||
path = self._search(service_dependency, visiting, path)
|
||||
visiting.remove(service.name)
|
||||
if path is None:
|
||||
path = []
|
||||
self.boot_paths.append(path)
|
||||
path.append(service)
|
||||
self.paths[service.name] = path
|
||||
return path
|
||||
|
||||
:return: list of services to boot, in order
|
||||
"""
|
||||
paths = []
|
||||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.booted:
|
||||
logging.debug(
|
||||
"skipping service that will already be booted: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
||||
path = self._start(service)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
if self.booted != set(self.node_services):
|
||||
raise ValueError(
|
||||
"failure to boot all services: %s != %s"
|
||||
% (self.booted, self.node_services.keys())
|
||||
)
|
||||
|
||||
return paths
|
||||
|
||||
def _reset(self) -> None:
|
||||
self.path = []
|
||||
self.visited.clear()
|
||||
self.visiting.clear()
|
||||
|
||||
def _start(self, service: "CoreService") -> List["CoreService"]:
|
||||
logging.debug("starting service dependency check: %s", service.name)
|
||||
self._reset()
|
||||
return self._visit(service)
|
||||
|
||||
def _visit(self, current_service: "CoreService") -> List["CoreService"]:
|
||||
logging.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
self.visited.add(current_service.name)
|
||||
self.visiting.add(current_service.name)
|
||||
|
||||
# dive down
|
||||
for service_name in current_service.dependencies:
|
||||
if service_name not in self.node_services:
|
||||
raise ValueError(
|
||||
"required dependency was not included in node services: %s"
|
||||
% service_name
|
||||
)
|
||||
|
||||
if service_name in self.visiting:
|
||||
raise ValueError(
|
||||
"cyclic dependency at service(%s): %s"
|
||||
% (current_service.name, service_name)
|
||||
)
|
||||
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
# add service when bottom is found
|
||||
logging.debug("adding service to boot path: %s", current_service.name)
|
||||
self.booted.add(current_service.name)
|
||||
self.path.append(current_service)
|
||||
self.visiting.remove(current_service.name)
|
||||
|
||||
# rise back up
|
||||
for service_name in self.dependents.get(current_service.name, []):
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
return self.path
|
||||
def boot_order(self) -> List[List["CoreServiceType"]]:
|
||||
for service in self.roots:
|
||||
self._search(service)
|
||||
return self.boot_paths
|
||||
|
||||
|
||||
class ServiceShim:
|
||||
|
@ -470,23 +443,16 @@ class CoreServices:
|
|||
:param node: node to start services on
|
||||
:return: nothing
|
||||
"""
|
||||
boot_paths = ServiceDependencies(node.services).boot_paths()
|
||||
boot_paths = ServiceDependencies(node.services).boot_order()
|
||||
funcs = []
|
||||
for boot_path in boot_paths:
|
||||
args = (node, boot_path)
|
||||
funcs.append((self._start_boot_paths, args, {}))
|
||||
funcs.append((self._boot_service_path, args, {}))
|
||||
result, exceptions = utils.threadpool(funcs)
|
||||
if exceptions:
|
||||
raise ServiceBootError(*exceptions)
|
||||
|
||||
def _start_boot_paths(self, node: CoreNode, boot_path: List["CoreService"]) -> None:
|
||||
"""
|
||||
Start all service boot paths found, based on dependencies.
|
||||
|
||||
:param node: node to start services on
|
||||
:param boot_path: service to start in dependent order
|
||||
:return: nothing
|
||||
"""
|
||||
def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]):
|
||||
logging.info(
|
||||
"booting node(%s) services: %s",
|
||||
node.name,
|
||||
|
@ -496,11 +462,11 @@ class CoreServices:
|
|||
service = self.get_service(node.id, service.name, default_service=True)
|
||||
try:
|
||||
self.boot_service(node, service)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logging.exception("exception booting service: %s", service.name)
|
||||
raise
|
||||
raise ServiceBootError(e)
|
||||
|
||||
def boot_service(self, node: CoreNode, service: "CoreService") -> None:
|
||||
def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None:
|
||||
"""
|
||||
Start a service on a node. Create private dirs, generate config
|
||||
files, and execute startup commands.
|
||||
|
@ -584,7 +550,7 @@ class CoreServices:
|
|||
return True
|
||||
return False
|
||||
|
||||
def validate_service(self, node: CoreNode, service: "CoreService") -> int:
|
||||
def validate_service(self, node: CoreNode, service: "CoreServiceType") -> int:
|
||||
"""
|
||||
Run the validation command(s) for a service.
|
||||
|
||||
|
@ -622,7 +588,7 @@ class CoreServices:
|
|||
for service in node.services:
|
||||
self.stop_service(node, service)
|
||||
|
||||
def stop_service(self, node: CoreNode, service: "CoreService") -> int:
|
||||
def stop_service(self, node: CoreNode, service: "CoreServiceType") -> int:
|
||||
"""
|
||||
Stop a service on a node.
|
||||
|
||||
|
@ -724,7 +690,7 @@ class CoreServices:
|
|||
service.config_data[file_name] = data
|
||||
|
||||
def startup_service(
|
||||
self, node: CoreNode, service: "CoreService", wait: bool = False
|
||||
self, node: CoreNode, service: "CoreServiceType", wait: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Startup a node service.
|
||||
|
@ -747,7 +713,7 @@ class CoreServices:
|
|||
status = -1
|
||||
return status
|
||||
|
||||
def create_service_files(self, node: CoreNode, service: "CoreService") -> None:
|
||||
def create_service_files(self, node: CoreNode, service: "CoreServiceType") -> None:
|
||||
"""
|
||||
Creates node service files.
|
||||
|
||||
|
|
211
daemon/poetry.lock
generated
211
daemon/poetry.lock
generated
|
@ -21,21 +21,20 @@ description = "Classes Without Boilerplate"
|
|||
name = "attrs"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "19.3.0"
|
||||
version = "20.1.0"
|
||||
|
||||
[package.extras]
|
||||
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
|
||||
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
|
||||
docs = ["sphinx", "zope.interface"]
|
||||
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
|
||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Modern password hashing for your software and your servers"
|
||||
name = "bcrypt"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "3.1.7"
|
||||
python-versions = ">=3.6"
|
||||
version = "3.2.0"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.1"
|
||||
|
@ -43,6 +42,7 @@ six = ">=1.4.1"
|
|||
|
||||
[package.extras]
|
||||
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"]
|
||||
typecheck = ["mypy"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -67,7 +67,7 @@ description = "Foreign Function Interface for Python calling C code."
|
|||
name = "cffi"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.14.0"
|
||||
version = "1.14.2"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
@ -103,23 +103,24 @@ description = "cryptography is a package which provides cryptographic recipes an
|
|||
name = "cryptography"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
version = "2.9.2"
|
||||
version = "3.0"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.8,<1.11.3 || >1.11.3"
|
||||
six = ">=1.4.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
|
||||
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"]
|
||||
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
idna = ["idna (>=2.1)"]
|
||||
pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
|
||||
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A backport of the dataclasses module for Python 3.6"
|
||||
marker = "python_version == \"3.6\""
|
||||
marker = "python_version >= \"3.6\" and python_version < \"3.7\""
|
||||
name = "dataclasses"
|
||||
optional = false
|
||||
python-versions = ">=3.6, <3.7"
|
||||
|
@ -131,7 +132,7 @@ description = "Distribution utilities"
|
|||
name = "distlib"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -203,7 +204,7 @@ description = "File identification library for Python"
|
|||
name = "identify"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
version = "1.4.18"
|
||||
version = "1.4.28"
|
||||
|
||||
[package.extras]
|
||||
license = ["editdistance"]
|
||||
|
@ -215,14 +216,14 @@ marker = "python_version < \"3.8\""
|
|||
name = "importlib-metadata"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "rst.linker"]
|
||||
testing = ["packaging", "importlib-resources"]
|
||||
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -231,13 +232,9 @@ marker = "python_version < \"3.7\""
|
|||
name = "importlib-resources"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
version = "1.5.0"
|
||||
version = "3.0.0"
|
||||
|
||||
[package.dependencies]
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = "*"
|
||||
|
||||
[package.dependencies.zipp]
|
||||
python = "<3.8"
|
||||
version = ">=0.4"
|
||||
|
@ -331,7 +328,7 @@ description = "More routines for operating on iterables, beyond itertools"
|
|||
name = "more-itertools"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "8.3.0"
|
||||
version = "8.4.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
|
@ -446,7 +443,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
|
|||
name = "py"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -560,11 +557,11 @@ description = "Virtual Python Environment builder"
|
|||
name = "virtualenv"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
version = "20.0.21"
|
||||
version = "20.0.31"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = ">=1.4.3,<2"
|
||||
distlib = ">=0.3.0,<1"
|
||||
distlib = ">=0.3.1,<1"
|
||||
filelock = ">=3.0.0,<4"
|
||||
six = ">=1.9.0,<2"
|
||||
|
||||
|
@ -574,11 +571,11 @@ version = ">=0.12,<2"
|
|||
|
||||
[package.dependencies.importlib-resources]
|
||||
python = "<3.7"
|
||||
version = ">=1.0,<2"
|
||||
version = ">=1.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"]
|
||||
testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
|
||||
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
|
||||
testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -586,7 +583,7 @@ description = "Measures the displayed width of unicode strings in a terminal"
|
|||
name = "wcwidth"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
@ -602,7 +599,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
|||
testing = ["jaraco.itertools", "func-timeout"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "94df87a12a92ccb6512e4c30965e7ba1fe54b4fa3ff75827ca55b3de8472b30e"
|
||||
content-hash = "cd09344b4f0183ada890fa9ac205e6d6410d94863e9067b5d2957274cebf374b"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
|
@ -615,62 +612,51 @@ atomicwrites = [
|
|||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
||||
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
|
||||
{file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"},
|
||||
{file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"},
|
||||
]
|
||||
bcrypt = [
|
||||
{file = "bcrypt-3.1.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7"},
|
||||
{file = "bcrypt-3.1.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31"},
|
||||
{file = "bcrypt-3.1.7-cp27-cp27m-win32.whl", hash = "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161"},
|
||||
{file = "bcrypt-3.1.7-cp27-cp27m-win_amd64.whl", hash = "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e"},
|
||||
{file = "bcrypt-3.1.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0"},
|
||||
{file = "bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052"},
|
||||
{file = "bcrypt-3.1.7-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105"},
|
||||
{file = "bcrypt-3.1.7-cp34-cp34m-win32.whl", hash = "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de"},
|
||||
{file = "bcrypt-3.1.7-cp34-cp34m-win_amd64.whl", hash = "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133"},
|
||||
{file = "bcrypt-3.1.7-cp35-cp35m-win32.whl", hash = "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5"},
|
||||
{file = "bcrypt-3.1.7-cp35-cp35m-win_amd64.whl", hash = "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09"},
|
||||
{file = "bcrypt-3.1.7-cp36-cp36m-win32.whl", hash = "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c"},
|
||||
{file = "bcrypt-3.1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89"},
|
||||
{file = "bcrypt-3.1.7-cp37-cp37m-win32.whl", hash = "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294"},
|
||||
{file = "bcrypt-3.1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"},
|
||||
{file = "bcrypt-3.1.7-cp38-cp38-win32.whl", hash = "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1"},
|
||||
{file = "bcrypt-3.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752"},
|
||||
{file = "bcrypt-3.1.7.tar.gz", hash = "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"},
|
||||
{file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"},
|
||||
{file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"},
|
||||
{file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
|
||||
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
|
||||
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
|
||||
{file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
|
||||
{file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
|
||||
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
|
||||
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
|
||||
{file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
|
||||
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
|
||||
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
|
||||
{file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
|
||||
{file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
|
||||
{file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
|
||||
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
|
||||
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
|
||||
{file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
|
||||
{file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
|
||||
{file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
|
||||
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
|
||||
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
|
||||
{file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
|
||||
{file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
|
||||
{file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
|
||||
{file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
|
||||
{file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
|
||||
{file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
|
||||
{file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
|
||||
{file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
|
||||
{file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"},
|
||||
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"},
|
||||
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"},
|
||||
{file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"},
|
||||
{file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"},
|
||||
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"},
|
||||
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"},
|
||||
{file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"},
|
||||
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"},
|
||||
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"},
|
||||
{file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"},
|
||||
{file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"},
|
||||
{file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"},
|
||||
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"},
|
||||
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"},
|
||||
{file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"},
|
||||
{file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"},
|
||||
{file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"},
|
||||
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"},
|
||||
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"},
|
||||
{file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"},
|
||||
{file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"},
|
||||
{file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"},
|
||||
{file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"},
|
||||
{file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"},
|
||||
{file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"},
|
||||
{file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"},
|
||||
{file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"},
|
||||
]
|
||||
cfgv = [
|
||||
{file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"},
|
||||
|
@ -685,32 +671,33 @@ colorama = [
|
|||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"},
|
||||
{file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"},
|
||||
{file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"},
|
||||
{file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"},
|
||||
{file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"},
|
||||
{file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"},
|
||||
{file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"},
|
||||
{file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"},
|
||||
{file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"},
|
||||
{file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"},
|
||||
{file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"},
|
||||
{file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"},
|
||||
{file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
|
||||
{file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"},
|
||||
{file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"},
|
||||
{file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"},
|
||||
{file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"},
|
||||
{file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"},
|
||||
{file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"},
|
||||
{file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"},
|
||||
{file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"},
|
||||
{file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"},
|
||||
{file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"},
|
||||
{file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"},
|
||||
{file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"},
|
||||
{file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"},
|
||||
{file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"},
|
||||
{file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"},
|
||||
{file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"},
|
||||
]
|
||||
dataclasses = [
|
||||
{file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"},
|
||||
{file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"},
|
||||
]
|
||||
distlib = [
|
||||
{file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"},
|
||||
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
|
||||
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
|
||||
]
|
||||
fabric = [
|
||||
{file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"},
|
||||
|
@ -815,16 +802,16 @@ grpcio-tools = [
|
|||
{file = "grpcio_tools-1.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:84724458c86ff9b14c29b49e321f34d80445b379f4cd4d0494c694b49b1d6f88"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-1.4.18-py2.py3-none-any.whl", hash = "sha256:9f53e80371f2ac7c969eefda8efaabd4f77c6300f5f8fc4b634744a0db8fe5cc"},
|
||||
{file = "identify-1.4.18.tar.gz", hash = "sha256:de4e1de6c23f52b71c8a54ff558219f3783ff011b432f29360d84a8a31ba561c"},
|
||||
{file = "identify-1.4.28-py2.py3-none-any.whl", hash = "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6"},
|
||||
{file = "identify-1.4.28.tar.gz", hash = "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"},
|
||||
{file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"},
|
||||
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
|
||||
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
|
||||
]
|
||||
importlib-resources = [
|
||||
{file = "importlib_resources-1.5.0-py2.py3-none-any.whl", hash = "sha256:85dc0b9b325ff78c8bef2e4ff42616094e16b98ebd5e3b50fe7e2f0bbcdcde49"},
|
||||
{file = "importlib_resources-1.5.0.tar.gz", hash = "sha256:6f87df66833e1942667108628ec48900e02a4ab4ad850e25fbf07cb17cf734ca"},
|
||||
{file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"},
|
||||
{file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"},
|
||||
]
|
||||
invoke = [
|
||||
{file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"},
|
||||
|
@ -912,8 +899,8 @@ mock = [
|
|||
{file = "mock-4.0.2.tar.gz", hash = "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"},
|
||||
]
|
||||
more-itertools = [
|
||||
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
|
||||
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
|
||||
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
|
||||
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
|
||||
]
|
||||
netaddr = [
|
||||
{file = "netaddr-0.7.19-py2.py3-none-any.whl", hash = "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"},
|
||||
|
@ -984,8 +971,8 @@ protobuf = [
|
|||
{file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
|
||||
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
|
||||
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
|
||||
|
@ -1072,12 +1059,12 @@ toml = [
|
|||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.0.21-py2.py3-none-any.whl", hash = "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70"},
|
||||
{file = "virtualenv-20.0.21.tar.gz", hash = "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf"},
|
||||
{file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"},
|
||||
{file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"},
|
||||
{file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"},
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
|
||||
|
|
|
@ -714,6 +714,19 @@ message Session {
|
|||
repeated Node nodes = 3;
|
||||
repeated Link links = 4;
|
||||
string dir = 5;
|
||||
string user = 6;
|
||||
repeated services.ServiceDefaults default_services = 7;
|
||||
SessionLocation location = 8;
|
||||
repeated Hook hooks = 9;
|
||||
repeated string emane_models = 10;
|
||||
map<string, common.ConfigOption> emane_config = 11;
|
||||
repeated emane.GetEmaneModelConfig emane_model_configs = 12;
|
||||
map<int32, common.MappedConfig> wlan_configs = 13;
|
||||
repeated services.NodeServiceConfig service_configs = 14;
|
||||
repeated configservices.ConfigServiceConfig config_service_configs = 15;
|
||||
map<int32, common.MappedConfig> mobility_configs = 16;
|
||||
map<string, string> metadata = 17;
|
||||
string file = 18;
|
||||
}
|
||||
|
||||
message SessionSummary {
|
||||
|
|
|
@ -53,14 +53,15 @@ message GetEmaneModelConfigsRequest {
|
|||
int32 session_id = 1;
|
||||
}
|
||||
|
||||
message GetEmaneModelConfig {
|
||||
int32 node_id = 1;
|
||||
string model = 2;
|
||||
int32 iface_id = 3;
|
||||
map<string, common.ConfigOption> config = 4;
|
||||
}
|
||||
|
||||
message GetEmaneModelConfigsResponse {
|
||||
message ModelConfig {
|
||||
int32 node_id = 1;
|
||||
string model = 2;
|
||||
int32 iface_id = 3;
|
||||
map<string, common.ConfigOption> config = 4;
|
||||
}
|
||||
repeated ModelConfig configs = 1;
|
||||
repeated GetEmaneModelConfig configs = 1;
|
||||
}
|
||||
|
||||
message GetEmaneEventChannelRequest {
|
||||
|
|
|
@ -59,6 +59,13 @@ message NodeServiceData {
|
|||
string meta = 10;
|
||||
}
|
||||
|
||||
message NodeServiceConfig {
|
||||
int32 node_id = 1;
|
||||
string service = 2;
|
||||
NodeServiceData data = 3;
|
||||
map<string, string> files = 4;
|
||||
}
|
||||
|
||||
message GetServicesRequest {
|
||||
|
||||
}
|
||||
|
@ -89,13 +96,7 @@ message GetNodeServiceConfigsRequest {
|
|||
}
|
||||
|
||||
message GetNodeServiceConfigsResponse {
|
||||
message ServiceConfig {
|
||||
int32 node_id = 1;
|
||||
string service = 2;
|
||||
NodeServiceData data = 3;
|
||||
map<string, string> files = 4;
|
||||
}
|
||||
repeated ServiceConfig configs = 1;
|
||||
repeated NodeServiceConfig configs = 1;
|
||||
}
|
||||
|
||||
message GetNodeServiceRequest {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "core"
|
||||
version = "7.0.1"
|
||||
version = "7.1.0"
|
||||
description = "CORE Common Open Research Emulator"
|
||||
authors = ["Boeing Research and Technology"]
|
||||
license = "BSD-2-Clause"
|
||||
|
@ -11,25 +11,25 @@ include = ["core/gui/data/**/*", "core/configservices/*/templates"]
|
|||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
dataclasses = { version = "*", python = "~3.6" }
|
||||
fabric = "*"
|
||||
fabric = "2.5.0"
|
||||
grpcio = "1.27.2"
|
||||
invoke = "*"
|
||||
lxml = "*"
|
||||
mako = "*"
|
||||
netaddr = "*"
|
||||
pillow = "*"
|
||||
protobuf = "*"
|
||||
pyproj = "*"
|
||||
pyyaml = "*"
|
||||
invoke = "1.4.1"
|
||||
lxml = "4.5.1"
|
||||
mako = "1.1.3"
|
||||
netaddr = "0.7.19"
|
||||
pillow = "7.1.2"
|
||||
protobuf = "3.12.2"
|
||||
pyproj = "2.6.1.post1"
|
||||
pyyaml = "5.3.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "==19.3b0"
|
||||
flake8 = "*"
|
||||
flake8 = "3.8.2"
|
||||
grpcio-tools = "1.27.2"
|
||||
isort = "*"
|
||||
mock = "*"
|
||||
pre-commit = "*"
|
||||
pytest = "*"
|
||||
isort = "4.3.21"
|
||||
mock = "4.0.2"
|
||||
pre-commit = "2.1.1"
|
||||
pytest = "5.4.3"
|
||||
|
||||
[tool.isort]
|
||||
skip_glob = "*_pb2*.py,doc,build"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import itertools
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
@ -15,40 +16,6 @@ SERVICE_ONE = "MyService"
|
|||
SERVICE_TWO = "MyService2"
|
||||
|
||||
|
||||
class ServiceA(CoreService):
|
||||
name = "A"
|
||||
dependencies = ("B",)
|
||||
|
||||
|
||||
class ServiceB(CoreService):
|
||||
name = "B"
|
||||
dependencies = ()
|
||||
|
||||
|
||||
class ServiceC(CoreService):
|
||||
name = "C"
|
||||
dependencies = ("B", "D")
|
||||
|
||||
|
||||
class ServiceD(CoreService):
|
||||
name = "D"
|
||||
dependencies = ()
|
||||
|
||||
|
||||
class ServiceBadDependency(CoreService):
|
||||
name = "E"
|
||||
dependencies = ("Z",)
|
||||
|
||||
|
||||
class ServiceF(CoreService):
|
||||
name = "F"
|
||||
dependencies = ()
|
||||
|
||||
|
||||
class ServiceCycleDependency(CoreService):
|
||||
name = "G"
|
||||
|
||||
|
||||
class TestServices:
|
||||
def test_service_all_files(self, session: Session):
|
||||
# given
|
||||
|
@ -253,37 +220,157 @@ class TestServices:
|
|||
assert default_service == my_service
|
||||
assert custom_service and custom_service != my_service
|
||||
|
||||
def test_services_dependencies(self):
|
||||
def test_services_dependency(self):
|
||||
# given
|
||||
services = [ServiceA, ServiceB, ServiceC, ServiceD, ServiceF]
|
||||
service_a = CoreService()
|
||||
service_a.name = "a"
|
||||
service_b = CoreService()
|
||||
service_b.name = "b"
|
||||
service_c = CoreService()
|
||||
service_c.name = "c"
|
||||
service_d = CoreService()
|
||||
service_d.name = "d"
|
||||
service_e = CoreService()
|
||||
service_e.name = "e"
|
||||
service_a.dependencies = (service_b.name,)
|
||||
service_b.dependencies = ()
|
||||
service_c.dependencies = (service_b.name, service_d.name)
|
||||
service_d.dependencies = ()
|
||||
service_e.dependencies = ()
|
||||
services = [service_a, service_b, service_c, service_d, service_e]
|
||||
expected1 = {service_a.name, service_b.name, service_c.name, service_d.name}
|
||||
expected2 = [service_e]
|
||||
|
||||
# when
|
||||
boot_paths = ServiceDependencies(services).boot_paths()
|
||||
permutations = itertools.permutations(services)
|
||||
for permutation in permutations:
|
||||
permutation = list(permutation)
|
||||
results = ServiceDependencies(permutation).boot_order()
|
||||
# then
|
||||
for result in results:
|
||||
result_set = {x.name for x in result}
|
||||
if len(result) == 4:
|
||||
a_index = result.index(service_a)
|
||||
b_index = result.index(service_b)
|
||||
c_index = result.index(service_c)
|
||||
d_index = result.index(service_d)
|
||||
assert b_index < a_index
|
||||
assert b_index < c_index
|
||||
assert d_index < c_index
|
||||
assert result_set == expected1
|
||||
elif len(result) == 1:
|
||||
assert expected2 == result
|
||||
else:
|
||||
raise ValueError(
|
||||
f"unexpected result: {results}, perm({permutation})"
|
||||
)
|
||||
|
||||
# then
|
||||
assert len(boot_paths) == 2
|
||||
|
||||
def test_services_dependencies_not_present(self):
|
||||
def test_services_dependency_missing(self):
|
||||
# given
|
||||
services = [
|
||||
ServiceA,
|
||||
ServiceB,
|
||||
ServiceC,
|
||||
ServiceD,
|
||||
ServiceF,
|
||||
ServiceBadDependency,
|
||||
]
|
||||
service_a = CoreService()
|
||||
service_a.name = "a"
|
||||
service_b = CoreService()
|
||||
service_b.name = "b"
|
||||
service_c = CoreService()
|
||||
service_c.name = "c"
|
||||
service_a.dependencies = (service_b.name,)
|
||||
service_b.dependencies = (service_c.name,)
|
||||
service_c.dependencies = ("d",)
|
||||
services = [service_a, service_b, service_c]
|
||||
|
||||
# when, then
|
||||
with pytest.raises(ValueError):
|
||||
ServiceDependencies(services).boot_paths()
|
||||
permutations = itertools.permutations(services)
|
||||
for permutation in permutations:
|
||||
permutation = list(permutation)
|
||||
with pytest.raises(ValueError):
|
||||
ServiceDependencies(permutation).boot_order()
|
||||
|
||||
def test_services_dependencies_cycle(self):
|
||||
def test_services_dependency_cycle(self):
|
||||
# given
|
||||
service_d = ServiceD()
|
||||
service_d.dependencies = ("C",)
|
||||
services = [ServiceA, ServiceB, ServiceC, service_d, ServiceF]
|
||||
service_a = CoreService()
|
||||
service_a.name = "a"
|
||||
service_b = CoreService()
|
||||
service_b.name = "b"
|
||||
service_c = CoreService()
|
||||
service_c.name = "c"
|
||||
service_a.dependencies = (service_b.name,)
|
||||
service_b.dependencies = (service_c.name,)
|
||||
service_c.dependencies = (service_a.name,)
|
||||
services = [service_a, service_b, service_c]
|
||||
|
||||
# when, then
|
||||
with pytest.raises(ValueError):
|
||||
ServiceDependencies(services).boot_paths()
|
||||
permutations = itertools.permutations(services)
|
||||
for permutation in permutations:
|
||||
permutation = list(permutation)
|
||||
with pytest.raises(ValueError):
|
||||
ServiceDependencies(permutation).boot_order()
|
||||
|
||||
def test_services_dependency_common(self):
|
||||
# given
|
||||
service_a = CoreService()
|
||||
service_a.name = "a"
|
||||
service_b = CoreService()
|
||||
service_b.name = "b"
|
||||
service_c = CoreService()
|
||||
service_c.name = "c"
|
||||
service_d = CoreService()
|
||||
service_d.name = "d"
|
||||
service_a.dependencies = (service_b.name,)
|
||||
service_c.dependencies = (service_d.name, service_b.name)
|
||||
services = [service_a, service_b, service_c, service_d]
|
||||
expected = {service_a.name, service_b.name, service_c.name, service_d.name}
|
||||
|
||||
# when
|
||||
permutations = itertools.permutations(services)
|
||||
for permutation in permutations:
|
||||
permutation = list(permutation)
|
||||
results = ServiceDependencies(permutation).boot_order()
|
||||
|
||||
# then
|
||||
for result in results:
|
||||
assert len(result) == 4
|
||||
result_set = {x.name for x in result}
|
||||
a_index = result.index(service_a)
|
||||
b_index = result.index(service_b)
|
||||
c_index = result.index(service_c)
|
||||
d_index = result.index(service_d)
|
||||
assert b_index < a_index
|
||||
assert d_index < c_index
|
||||
assert b_index < c_index
|
||||
assert expected == result_set
|
||||
|
||||
def test_services_dependency_common2(self):
|
||||
# given
|
||||
service_a = CoreService()
|
||||
service_a.name = "a"
|
||||
service_b = CoreService()
|
||||
service_b.name = "b"
|
||||
service_c = CoreService()
|
||||
service_c.name = "c"
|
||||
service_d = CoreService()
|
||||
service_d.name = "d"
|
||||
service_a.dependencies = (service_b.name,)
|
||||
service_b.dependencies = (service_c.name, service_d.name)
|
||||
service_c.dependencies = (service_d.name,)
|
||||
services = [service_a, service_b, service_c, service_d]
|
||||
expected = {service_a.name, service_b.name, service_c.name, service_d.name}
|
||||
|
||||
# when
|
||||
permutations = itertools.permutations(services)
|
||||
for permutation in permutations:
|
||||
permutation = list(permutation)
|
||||
results = ServiceDependencies(permutation).boot_order()
|
||||
|
||||
# then
|
||||
for result in results:
|
||||
assert len(result) == 4
|
||||
result_set = {x.name for x in result}
|
||||
a_index = result.index(service_a)
|
||||
b_index = result.index(service_b)
|
||||
c_index = result.index(service_c)
|
||||
d_index = result.index(service_d)
|
||||
assert b_index < a_index
|
||||
assert c_index < b_index
|
||||
assert d_index < b_index
|
||||
assert d_index < c_index
|
||||
assert expected == result_set
|
||||
|
|
|
@ -50,23 +50,6 @@ can also subscribe to EMANE location events and move the nodes on the canvas
|
|||
as they are moved in the EMANE emulation. This would occur when an Emulation
|
||||
Script Generator, for example, is running a mobility script.
|
||||
|
||||
## EMANE Installation
|
||||
|
||||
EMANE can be installed from deb or RPM packages or from source. See the
|
||||
[EMANE GitHub](https://github.com/adjacentlink/emane) for full details.
|
||||
|
||||
Here are quick instructions for installing all EMANE packages for Ubuntu 18.04:
|
||||
```shell
|
||||
# install dependencies
|
||||
sudo apt-get install libssl-dev libxml-libxml-perl libxml-simple-perl
|
||||
wget https://adjacentlink.com/downloads/emane/emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
|
||||
tar xzf emane-1.2.5-release-1.ubuntu-18_04.amd64.tar.gz
|
||||
# install base emane packages
|
||||
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/emane*.deb
|
||||
# install python3 bindings
|
||||
sudo dpkg -i emane-1.2.5-release-1/deb/ubuntu-18_04/amd64/python3*.deb
|
||||
```
|
||||
|
||||
## EMANE Configuration
|
||||
|
||||
The CORE configuration file **/etc/core/core.conf** has options specific to
|
||||
|
|
|
@ -132,30 +132,14 @@ After the installation complete it will have installed the following scripts.
|
|||
|
||||
If you create your own python scripts to run CORE directly or using the gRPC/TLV
|
||||
APIs you will need to make sure you are running them within context of the
|
||||
installed virtual environment.
|
||||
installed virtual environment. To help support this CORE provides the `core-python`
|
||||
executable. This executable will allow you to enter CORE's python virtual
|
||||
environment interpreter or to run a script within it.
|
||||
|
||||
> **NOTE:** the following assumes CORE has been installed successfully
|
||||
|
||||
There is an invoke task to help with this case.
|
||||
```shell
|
||||
cd <CORE_REPO>
|
||||
inv -h run
|
||||
Usage: inv[oke] [--core-opts] run [--options] [other tasks here ...]
|
||||
|
||||
Docstring:
|
||||
runs a user script in the core virtual environment
|
||||
|
||||
Options:
|
||||
-f STRING, --file=STRING script file to run in the core virtual environment
|
||||
-s, --sudo run script as sudo
|
||||
```
|
||||
|
||||
Another way would be to enable the core virtual environment shell. Which
|
||||
would allow you to run scripts in a more **normal** way.
|
||||
```shell
|
||||
cd <CORE_REPO>/daemon
|
||||
poetry shell
|
||||
python run /path/to/script.py
|
||||
core-python <script>
|
||||
```
|
||||
|
||||
## Manually Install EMANE
|
||||
|
@ -199,7 +183,6 @@ Available tasks:
|
|||
install-emane install emane and the python bindings
|
||||
install-scripts install core script files, modified to leverage virtual environment
|
||||
install-service install systemd core service
|
||||
run runs a user script in the core virtual environment
|
||||
test run core tests
|
||||
test-emane run core emane tests
|
||||
test-mock run core tests using mock to avoid running as sudo
|
||||
|
|
40
tasks.py
40
tasks.py
|
@ -271,6 +271,18 @@ def install_scripts(c, verbose=False, prefix=DEFAULT_PREFIX):
|
|||
else:
|
||||
c.run(f"sudo cp {script} {dest}", hide=hide)
|
||||
|
||||
# setup core python helper
|
||||
core_python = bin_dir.joinpath("core-python")
|
||||
temp = NamedTemporaryFile("w", delete=False)
|
||||
temp.writelines([
|
||||
"#!/bin/bash\n",
|
||||
f'exec "{python}" "$@"\n',
|
||||
])
|
||||
temp.close()
|
||||
c.run(f"sudo cp {temp.name} {core_python}", hide=hide)
|
||||
c.run(f"sudo chmod 755 {core_python}", hide=hide)
|
||||
os.unlink(temp.name)
|
||||
|
||||
# install core configuration file
|
||||
config_dir = "/etc/core"
|
||||
c.run(f"sudo mkdir -p {config_dir}", hide=hide)
|
||||
|
@ -312,7 +324,7 @@ def install(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX):
|
|||
install_service(c, hide, prefix)
|
||||
with p.start("installing ospf mdr"):
|
||||
install_ospf_mdr(c, os_info, hide)
|
||||
print("\nyou may need to open a new terminal to leverage invoke for running core")
|
||||
print("\ninstall complete!")
|
||||
|
||||
|
||||
@task(
|
||||
|
@ -402,6 +414,10 @@ def uninstall(c, dev=False, verbose=False, prefix=DEFAULT_PREFIX):
|
|||
dest = bin_dir.joinpath(script.name)
|
||||
c.run(f"sudo rm -f {dest}", hide=hide)
|
||||
|
||||
# remove core-python symlink
|
||||
core_python = bin_dir.joinpath("core-python")
|
||||
c.run(f"sudo rm -f {core_python}", hide=hide)
|
||||
|
||||
# install service
|
||||
systemd_dir = Path("/lib/systemd/system/")
|
||||
service_name = "core-daemon.service"
|
||||
|
@ -453,28 +469,6 @@ def daemon(c):
|
|||
)
|
||||
|
||||
|
||||
@task(
|
||||
help={
|
||||
"sudo": "run script as sudo",
|
||||
"file": "script file to run in the core virtual environment"
|
||||
},
|
||||
)
|
||||
def run(c, file, sudo=False):
|
||||
"""
|
||||
runs a user script in the core virtual environment
|
||||
"""
|
||||
if not file:
|
||||
print("no script was provided")
|
||||
return
|
||||
python = get_python(c)
|
||||
path = Path(file).absolute()
|
||||
with c.cd(DAEMON_DIR):
|
||||
cmd = f"{python} {path}"
|
||||
if sudo:
|
||||
cmd = f"sudo {cmd}"
|
||||
c.run(cmd, pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def test(c):
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue