Merge pull request #357 from coreemu/feature/config-service
Feature/config service
This commit is contained in:
commit
c5666f9112
81 changed files with 5044 additions and 187 deletions
|
@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Dict, List
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import client, core_pb2
|
||||
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2
|
||||
from core.gui import appconfig
|
||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
|
@ -74,6 +74,8 @@ class CoreClient:
|
|||
self.app = app
|
||||
self.master = app.master
|
||||
self.services = {}
|
||||
self.config_services_groups = {}
|
||||
self.config_services = {}
|
||||
self.default_services = {}
|
||||
self.emane_models = []
|
||||
self.observer = None
|
||||
|
@ -99,6 +101,7 @@ class CoreClient:
|
|||
self.emane_model_configs = {}
|
||||
self.emane_config = None
|
||||
self.service_configs = {}
|
||||
self.config_service_configs = {}
|
||||
self.file_configs = {}
|
||||
self.mobility_players = {}
|
||||
self.handling_throughputs = None
|
||||
|
@ -307,6 +310,18 @@ class CoreClient:
|
|||
data = config.files[file_name]
|
||||
files[file_name] = data
|
||||
|
||||
# get config service configurations
|
||||
response = self.client.get_node_config_service_configs(self.session_id)
|
||||
for config in response.configs:
|
||||
node_configs = self.config_service_configs.setdefault(
|
||||
config.node_id, {}
|
||||
)
|
||||
service_config = node_configs.setdefault(config.name, {})
|
||||
if config.templates:
|
||||
service_config["templates"] = config.templates
|
||||
if config.config:
|
||||
service_config["config"] = config.config
|
||||
|
||||
# draw session
|
||||
self.app.canvas.reset_and_redraw(session)
|
||||
|
||||
|
@ -427,6 +442,15 @@ class CoreClient:
|
|||
group_services = self.services.setdefault(service.group, set())
|
||||
group_services.add(service.name)
|
||||
|
||||
# get config service informations
|
||||
response = self.client.get_config_services()
|
||||
for service in response.services:
|
||||
self.config_services[service.name] = service
|
||||
group_services = self.config_services_groups.setdefault(
|
||||
service.group, set()
|
||||
)
|
||||
group_services.add(service.name)
|
||||
|
||||
# if there are no sessions, create a new session, else join a session
|
||||
response = self.client.get_sessions()
|
||||
logging.info("current sessions: %s", response)
|
||||
|
@ -464,6 +488,7 @@ class CoreClient:
|
|||
asymmetric_links = [
|
||||
x.asymmetric_link for x in self.links.values() if x.asymmetric_link
|
||||
]
|
||||
config_service_configs = self.get_config_service_configs_proto()
|
||||
if self.emane_config:
|
||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
else:
|
||||
|
@ -484,6 +509,7 @@ class CoreClient:
|
|||
service_configs,
|
||||
file_configs,
|
||||
asymmetric_links,
|
||||
config_service_configs,
|
||||
)
|
||||
logging.debug(
|
||||
"start session(%s), result: %s", self.session_id, response.result
|
||||
|
@ -878,18 +904,34 @@ class CoreClient:
|
|||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_config_service_configs_proto(
|
||||
self
|
||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
||||
config_service_protos = []
|
||||
for node_id, node_config in self.config_service_configs.items():
|
||||
for name, service_config in node_config.items():
|
||||
config = service_config.get("config", {})
|
||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
||||
node_id=node_id,
|
||||
name=name,
|
||||
templates=service_config["templates"],
|
||||
config=config,
|
||||
)
|
||||
config_service_protos.append(config_proto)
|
||||
return config_service_protos
|
||||
|
||||
def run(self, node_id: int) -> str:
|
||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||
|
||||
def get_wlan_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]:
|
||||
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.wlan_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
return config
|
||||
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]:
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.mobility_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||
|
@ -898,7 +940,7 @@ class CoreClient:
|
|||
|
||||
def get_emane_model_config(
|
||||
self, node_id: int, model: str, interface: int = None
|
||||
) -> Dict[str, core_pb2.ConfigOption]:
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
logging.info("getting emane model config: %s %s %s", node_id, model, interface)
|
||||
config = self.emane_model_configs.get((node_id, model, interface))
|
||||
if not config:
|
||||
|
@ -914,7 +956,7 @@ class CoreClient:
|
|||
self,
|
||||
node_id: int,
|
||||
model: str,
|
||||
config: Dict[str, core_pb2.ConfigOption],
|
||||
config: Dict[str, common_pb2.ConfigOption],
|
||||
interface: int = None,
|
||||
):
|
||||
logging.info("setting emane model config: %s %s %s", node_id, model, interface)
|
||||
|
|
381
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
381
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
|
@ -0,0 +1,381 @@
|
|||
"""
|
||||
Service configuration dialog
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ConfigServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: Any, app: "Application", service_name: str, node_id: int
|
||||
):
|
||||
title = f"{service_name} Config Service"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.master = master
|
||||
self.app = app
|
||||
self.core = app.core
|
||||
self.node_id = node_id
|
||||
self.service_name = service_name
|
||||
self.service_configs = app.core.config_service_configs
|
||||
|
||||
self.radiovar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.directories = []
|
||||
self.templates = []
|
||||
self.dependencies = []
|
||||
self.executables = []
|
||||
self.startup_commands = []
|
||||
self.validation_commands = []
|
||||
self.shutdown_commands = []
|
||||
self.default_startup = []
|
||||
self.default_validate = []
|
||||
self.default_shutdown = []
|
||||
self.validation_mode = None
|
||||
self.validation_time = None
|
||||
self.validation_period = tk.StringVar()
|
||||
self.modes = []
|
||||
self.mode_configs = {}
|
||||
|
||||
self.notebook = None
|
||||
self.templates_combobox = None
|
||||
self.modes_combobox = None
|
||||
self.startup_commands_listbox = None
|
||||
self.shutdown_commands_listbox = None
|
||||
self.validate_commands_listbox = None
|
||||
self.validation_time_entry = None
|
||||
self.validation_mode_entry = None
|
||||
self.template_text = None
|
||||
self.validation_period_entry = None
|
||||
self.original_service_files = {}
|
||||
self.temp_service_files = {}
|
||||
self.modified_files = set()
|
||||
self.config_frame = None
|
||||
self.default_config = None
|
||||
self.config = None
|
||||
self.load()
|
||||
self.draw()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
self.core.create_nodes_and_links()
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
self.directories = service.directories[:]
|
||||
self.templates = service.files[:]
|
||||
self.startup_commands = service.startup[:]
|
||||
self.validation_commands = service.validate[:]
|
||||
self.shutdown_commands = service.shutdown[:]
|
||||
self.validation_mode = service.validation_mode
|
||||
self.validation_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
|
||||
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}
|
||||
|
||||
node_configs = self.service_configs.get(self.node_id, {})
|
||||
service_config = node_configs.get(self.service_name, {})
|
||||
|
||||
self.config = 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():
|
||||
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
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_tab_files()
|
||||
if self.config:
|
||||
self.draw_tab_config()
|
||||
self.draw_tab_startstop()
|
||||
self.draw_tab_validation()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_tab_files(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
tab, text="Directories and templates that will be used for this service."
|
||||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
directories_combobox.grid(row=0, column=1, sticky="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)
|
||||
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.template_text = CodeText(tab)
|
||||
self.template_text.grid(sticky="nsew")
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert(
|
||||
"end", self.temp_service_files[self.templates[0]]
|
||||
)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
|
||||
def draw_tab_config(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="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.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Modes")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.modes_combobox = ttk.Combobox(
|
||||
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)
|
||||
|
||||
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)
|
||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||
|
||||
def draw_tab_startstop(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
self.notebook.add(tab, text="Startup/Shutdown")
|
||||
commands = []
|
||||
# tab 3
|
||||
for i in range(3):
|
||||
label_frame = None
|
||||
if i == 0:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Startup Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.startup_commands
|
||||
elif i == 1:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Shutdown Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.shutdown_commands
|
||||
elif i == 2:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Validation Commands", padding=FRAME_PAD
|
||||
)
|
||||
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)
|
||||
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")
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
self.shutdown_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 2:
|
||||
self.validate_commands_listbox = listbox_scroll.listbox
|
||||
|
||||
def draw_tab_validation(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="ew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Validation", sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="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)
|
||||
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)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING:
|
||||
mode = "NON_BLOCKING"
|
||||
else:
|
||||
mode = "TIMER"
|
||||
self.validation_mode_entry = ttk.Entry(
|
||||
frame, textvariable=tk.StringVar(value=mode)
|
||||
)
|
||||
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)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="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)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="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")
|
||||
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.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="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):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="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 = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom():
|
||||
if self.node_id in self.service_configs:
|
||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
node_config = self.service_configs.setdefault(self.node_id, {})
|
||||
service_config = node_config.setdefault(self.service_name, {})
|
||||
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", {})
|
||||
for file in self.modified_files:
|
||||
templates_config[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")
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event):
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
|
||||
def handle_mode_changed(self, event: tk.Event):
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logging.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event):
|
||||
scrolledtext = event.widget
|
||||
template = self.templates_combobox.get()
|
||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[template] != self.original_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
self.modified_files.discard(template)
|
||||
|
||||
def is_custom(self):
|
||||
has_custom_templates = len(self.modified_files) > 0
|
||||
has_custom_config = False
|
||||
if self.config_frame:
|
||||
current = self.config_frame.parse_config()
|
||||
has_custom_config = self.default_config != current
|
||||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self):
|
||||
if self.node_id in self.service_configs:
|
||||
node_config = self.service_configs.get(self.node_id, {})
|
||||
node_config.pop(self.service_name, None)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
filename = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[filename])
|
||||
if self.config_frame:
|
||||
logging.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
||||
def click_copy(self):
|
||||
pass
|
||||
|
||||
def append_commands(
|
||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||
):
|
||||
for cmd in to_add:
|
||||
commands.append(cmd)
|
||||
listbox.insert(tk.END, cmd)
|
161
daemon/core/gui/dialogs/nodeconfigservice.py
Normal file
161
daemon/core/gui/dialogs/nodeconfigservice.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
"""
|
||||
core node services
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import TYPE_CHECKING, Any, Set
|
||||
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class NodeConfigServiceDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
master: Any,
|
||||
app: "Application",
|
||||
canvas_node: "CanvasNode",
|
||||
services: Set[str] = None,
|
||||
):
|
||||
title = f"{canvas_node.core_node.name} Config Services"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.app = app
|
||||
self.canvas_node = canvas_node
|
||||
self.node_id = canvas_node.core_node.id
|
||||
self.groups = None
|
||||
self.services = None
|
||||
self.current = None
|
||||
if services is None:
|
||||
services = set(canvas_node.core_node.config_services)
|
||||
self.current_services = services
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="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.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="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.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")
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
if self.is_custom_service(service):
|
||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
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 = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.click_remove)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event: tk.Event = None):
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.config_services_groups[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
def service_clicked(self, name: str, var: tk.IntVar):
|
||||
if var.get() and name not in self.current_services:
|
||||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.current.listbox.delete(0, tk.END)
|
||||
for name in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, name)
|
||||
if self.is_custom_service(name):
|
||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
|
||||
def click_configure(self):
|
||||
current_selection = self.current.listbox.curselection()
|
||||
if len(current_selection):
|
||||
dialog = ConfigServiceConfigDialog(
|
||||
master=self,
|
||||
app=self.app,
|
||||
service_name=self.current.listbox.get(current_selection[0]),
|
||||
node_id=self.node_id,
|
||||
)
|
||||
dialog.show()
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Node service configuration", "Select a service to configure"
|
||||
)
|
||||
|
||||
def click_save(self):
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
logging.info(
|
||||
"saved node config services: %s", self.canvas_node.core_node.config_services
|
||||
)
|
||||
self.destroy()
|
||||
|
||||
def click_cancel(self):
|
||||
self.current_services = None
|
||||
self.destroy()
|
||||
|
||||
def click_remove(self):
|
||||
cur = self.current.listbox.curselection()
|
||||
if cur:
|
||||
service = self.current.listbox.get(cur[0])
|
||||
self.current.listbox.delete(cur[0])
|
||||
self.current_services.remove(service)
|
||||
for checkbutton in self.services.frame.winfo_children():
|
||||
if checkbutton["text"] == service:
|
||||
checkbutton.invoke()
|
||||
return
|
||||
|
||||
def is_custom_service(self, service: str) -> bool:
|
||||
node_configs = self.app.core.config_service_configs.get(self.node_id, {})
|
||||
service_config = node_configs.get(service)
|
||||
if node_configs and service_config:
|
||||
return True
|
||||
else:
|
||||
return False
|
|
@ -10,6 +10,7 @@ from core.gui import themes
|
|||
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
|
||||
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
|
||||
from core.gui.dialogs.nodeconfig import NodeConfigDialog
|
||||
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
|
||||
from core.gui.dialogs.nodeservice import NodeServiceDialog
|
||||
from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
|
@ -180,6 +181,7 @@ class CanvasNode:
|
|||
context.add_command(label="Configure", command=self.show_config)
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
context.add_command(label="Services", state=tk.DISABLED)
|
||||
context.add_command(label="Config Services", state=tk.DISABLED)
|
||||
if is_wlan:
|
||||
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:
|
||||
|
@ -198,6 +200,9 @@ class CanvasNode:
|
|||
context.add_command(label="Configure", command=self.show_config)
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
context.add_command(label="Services", command=self.show_services)
|
||||
context.add_command(
|
||||
label="Config Services", command=self.show_config_services
|
||||
)
|
||||
if is_emane:
|
||||
context.add_command(
|
||||
label="EMANE Config", command=self.show_emane_config
|
||||
|
@ -253,6 +258,11 @@ class CanvasNode:
|
|||
dialog = NodeServiceDialog(self.app.master, self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def show_config_services(self):
|
||||
self.canvas.context = None
|
||||
dialog = NodeConfigServiceDialog(self.app.master, self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def has_emane_link(self, interface_id: int) -> core_pb2.Node:
|
||||
result = None
|
||||
for edge in self.edges:
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import PosixPath
|
|||
from tkinter import filedialog, font, ttk
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.gui import themes
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
|
@ -74,7 +74,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
self,
|
||||
master: tk.Widget,
|
||||
app: "Application",
|
||||
config: Dict[str, core_pb2.ConfigOption],
|
||||
config: Dict[str, common_pb2.ConfigOption],
|
||||
**kw
|
||||
):
|
||||
super().__init__(master, **kw)
|
||||
|
@ -99,7 +99,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
label.grid(row=index, pady=PADY, padx=PADX, sticky="w")
|
||||
value = tk.StringVar()
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
select = tuple(option.select)
|
||||
select = ("On", "Off")
|
||||
combobox = ttk.Combobox(
|
||||
tab.frame, textvariable=value, values=select, state="readonly"
|
||||
)
|
||||
|
@ -184,6 +184,17 @@ class ConfigFrame(ttk.Notebook):
|
|||
|
||||
return {x: self.config[x].value for x in self.config}
|
||||
|
||||
def set_values(self, config: Dict[str, str]) -> None:
|
||||
for name, data in config.items():
|
||||
option = self.config[name]
|
||||
value = self.values[name]
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
if data == "1":
|
||||
data = "On"
|
||||
else:
|
||||
data = "Off"
|
||||
value.set(data)
|
||||
|
||||
|
||||
class ListboxScroll(ttk.Frame):
|
||||
def __init__(self, master: tk.Widget = None, **kw):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue