From 2e8bf71551c67f99fa6aee76c754485f3371282d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:19:30 -0800 Subject: [PATCH] daemon/gui: adjustments to support EMANE manifest parsing to use regexes directly and check them within the GUI, small adjustment when writing out compose files to avoid removing single quotes --- daemon/core/api/grpc/grpcutils.py | 1 + daemon/core/api/grpc/wrappers.py | 2 ++ daemon/core/config.py | 1 + daemon/core/emane/emanemanifest.py | 25 +++++-------------------- daemon/core/gui/dialogs/emaneconfig.py | 13 ++++++++----- daemon/core/gui/widgets.py | 16 +++++++++++++--- daemon/core/nodes/docker.py | 3 ++- daemon/core/nodes/podman.py | 3 ++- daemon/proto/core/api/grpc/common.proto | 1 + 9 files changed, 35 insertions(+), 30 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index a0613ec5b..14eb2fe96 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -266,6 +266,7 @@ def get_config_options( value=value, type=configuration.type.value, select=configuration.options, + regex=configuration.regex, ) results[configuration.id] = config_option for config_group in configurable_options.config_groups(): diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index ddf8e5853..3298ac7a9 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -307,6 +307,7 @@ class ConfigOption: type: ConfigOptionType = None group: str = None select: list[str] = None + regex: str = None @classmethod def from_dict( @@ -328,6 +329,7 @@ def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption": type=config_type, group=proto.group, select=list(proto.select), + regex=proto.regex, ) def to_proto(self) -> common_pb2.ConfigOption: diff --git a/daemon/core/config.py b/daemon/core/config.py index 7cf529a82..b1d9f2d1a 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -45,6 +45,7 @@ class Configuration: default: str = "" options: list[str] = field(default_factory=list) group: str = "Configuration" + regex: str = None def __post_init__(self) -> None: self.label = self.label if self.label else self.id diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index ea2b05fd7..2c6b1f617 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -32,24 +32,6 @@ def _type_value(config_type: str) -> ConfigDataTypes: return ConfigDataTypes[config_type] -def _get_possible(config_type: str, config_regex: str) -> list[str]: - """ - Retrieve possible config value options based on emane regexes. - - :param config_type: emane configuration type - :param config_regex: emane configuration regex - :return: a string listing comma delimited values, if needed, empty string otherwise - """ - if config_type == "bool": - return ["On", "Off"] - - if config_type == "string" and config_regex: - possible = config_regex[2:-2] - return possible.split("|") - - return [] - - def _get_default(config_type_name: str, config_value: list[str]) -> str: """ Convert default configuration values to one used by core. @@ -111,7 +93,9 @@ def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]: # map to possible values used as options within the gui config_regex = config_info.get("regex") - possible = _get_possible(config_type_name, config_regex) + options = None + if config_type == "bool": + options = ["On", "Off"] # define description and account for gui quirks config_descriptions = config_name @@ -122,8 +106,9 @@ def parse(manifest_path: Path, defaults: dict[str, str]) -> list[Configuration]: id=config_name, type=config_type_value, default=config_default, - options=possible, + options=options, label=config_descriptions, + regex=config_regex, ) configurations.append(configuration) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 00eda6948..7cea29c41 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -3,7 +3,7 @@ """ import tkinter as tk import webbrowser -from tkinter import ttk +from tkinter import messagebox, ttk from typing import TYPE_CHECKING, Optional import grpc @@ -70,10 +70,13 @@ def draw_buttons(self) -> None: 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.node.emane_model_configs[key] = self.config - self.destroy() + try: + self.config_frame.parse_config() + key = (self.model, self.iface_id) + self.node.emane_model_configs[key] = self.config + self.destroy() + except ValueError as e: + messagebox.showerror("EMANE Config Error", str(e)) class EmaneConfigDialog(Dialog): diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 902f11320..6191d887d 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -1,4 +1,5 @@ import logging +import re import tkinter as tk from functools import partial from pathlib import Path @@ -9,6 +10,7 @@ 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.tooltip import Tooltip logger = logging.getLogger(__name__) @@ -41,7 +43,7 @@ def __init__( master: tk.Widget, app: "Application", _cls: type[ttk.Frame] = ttk.Frame, - **kw: Any + **kw: Any, ) -> None: super().__init__(master, **kw) self.app: "Application" = app @@ -88,7 +90,7 @@ def __init__( app: "Application", config: dict[str, ConfigOption], enabled: bool = True, - **kw: Any + **kw: Any, ) -> None: super().__init__(master, **kw) self.app: "Application" = app @@ -148,6 +150,8 @@ def draw_config(self) -> None: else: entry = ttk.Entry(tab.frame, textvariable=value, state=state) entry.grid(row=index, column=1, sticky=tk.EW) + if option.regex: + Tooltip(entry, option.regex) elif option.type in INT_TYPES: value.set(option.value) state = tk.NORMAL if self.enabled else tk.DISABLED @@ -177,6 +181,12 @@ def parse_config(self) -> dict[str, str]: else: option.value = "0" else: + if option.regex: + if not re.match(option.regex, config_value): + raise ValueError( + f"{option.label} value '{config_value}' " + f"does not match regex '{option.regex}'" + ) option.value = config_value return {x: self.config[x].value for x in self.config} @@ -216,7 +226,7 @@ def __init__( master: ttk.Widget, app: "Application", clicked: Callable = None, - **kw: Any + **kw: Any, ) -> None: super().__init__(master, app, **kw) self.clicked: Callable = clicked diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index e587c24d9..db71bf069 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -206,9 +206,10 @@ def startup(self) -> None: data = self.host_cmd(f"cat {self.compose}") template = Template(data) rendered = template.render_unicode(node=self, hostname=hostname) + rendered = rendered.replace('"', r"\"") rendered = "\\n".join(rendered.splitlines()) compose_path = self.directory / "docker-compose.yml" - self.host_cmd(f"printf '{rendered}' >> {compose_path}", shell=True) + self.host_cmd(f'printf "{rendered}" >> {compose_path}', shell=True) self.host_cmd( f"{DOCKER_COMPOSE} up -d {self.compose_name}", cwd=self.directory, diff --git a/daemon/core/nodes/podman.py b/daemon/core/nodes/podman.py index dd6bcc361..bcfb087b7 100644 --- a/daemon/core/nodes/podman.py +++ b/daemon/core/nodes/podman.py @@ -160,9 +160,10 @@ def startup(self) -> None: data = self.host_cmd(f"cat {self.compose}") template = Template(data) rendered = template.render_unicode(node=self, hostname=hostname) + rendered = rendered.replace('"', r"\"") rendered = "\\n".join(rendered.splitlines()) compose_path = self.directory / "podman-compose.yml" - self.host_cmd(f"printf '{rendered}' >> {compose_path}", shell=True) + self.host_cmd(f'printf "{rendered}" >> {compose_path}', shell=True) self.host_cmd(f"{PODMAN_COMPOSE} up -d", cwd=self.directory) else: # setup commands for creating bind/volume mounts diff --git a/daemon/proto/core/api/grpc/common.proto b/daemon/proto/core/api/grpc/common.proto index 065bee7a3..060432551 100644 --- a/daemon/proto/core/api/grpc/common.proto +++ b/daemon/proto/core/api/grpc/common.proto @@ -9,6 +9,7 @@ message ConfigOption { int32 type = 4; repeated string select = 5; string group = 6; + string regex = 7; } message MappedConfig {