diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ba80ace8..0f939921 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -17,6 +17,8 @@ from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsResponse, GetConfigServicesRequest, GetConfigServicesResponse, + GetNodeConfigServiceConfigsRequest, + GetNodeConfigServiceConfigsResponse, GetNodeConfigServiceRequest, GetNodeConfigServiceResponse, GetNodeConfigServicesRequest, @@ -1103,6 +1105,12 @@ class CoreGrpcClient: request = GetConfigServiceDefaultsRequest(name=name) return self.stub.GetConfigServiceDefaults(request) + def get_node_config_service_configs( + self, session_id: int + ) -> GetNodeConfigServiceConfigsResponse: + request = GetNodeConfigServiceConfigsRequest(session_id=session_id) + return self.stub.GetNodeConfigServiceConfigs(request) + def get_node_config_service( self, session_id: int, node_id: int, name: str ) -> GetNodeConfigServiceResponse: diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 9b2634a9..eee6a446 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -23,6 +23,8 @@ from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsResponse, GetConfigServicesRequest, GetConfigServicesResponse, + GetNodeConfigServiceConfigsRequest, + GetNodeConfigServiceConfigsResponse, GetNodeConfigServiceRequest, GetNodeConfigServiceResponse, GetNodeConfigServicesRequest, @@ -45,7 +47,7 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility -from core.nodes.base import NodeBase +from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager @@ -192,7 +194,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if config.config: service.set_config(config.config) for name, template in config.templates.items(): - service.custom_template(name, template) + service.set_template(name, template) # service file configs for config in request.service_file_configs: @@ -463,6 +465,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if services is None: services = [] services = [x.name for x in services] + config_services = getattr(node, "config_services", {}) + config_services = [x for x in config_services] emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name @@ -478,6 +482,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): services=services, icon=node.icon, image=image, + config_services=config_services, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image @@ -1528,6 +1533,27 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): templates=templates, config=config, modes=modes ) + def GetNodeConfigServiceConfigs( + self, request: GetNodeConfigServiceConfigsRequest, context: ServicerContext + ) -> GetNodeConfigServiceConfigsResponse: + 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) + return GetNodeConfigServiceConfigsResponse(configs=configs) + def GetNodeConfigServices( self, request: GetNodeConfigServicesRequest, context: ServicerContext ) -> GetNodeConfigServicesResponse: diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index c14aec7e..990452cd 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -38,6 +38,7 @@ class ConfigService(abc.ABC): self.templates = TemplateLookup(directories=templates_path) self.config = {} self.custom_templates = {} + self.custom_config = {} configs = self.default_configs[:] self._define_config(configs) @@ -134,7 +135,7 @@ class ConfigService(abc.ABC): def data(self) -> Dict[str, Any]: return {} - def custom_template(self, name: str, template: str) -> None: + def set_template(self, name: str, template: str) -> None: self.custom_templates[name] = template def get_text(self, name: str) -> str: @@ -248,11 +249,13 @@ class ConfigService(abc.ABC): self.config[config.id] = config def render_config(self) -> Dict[str, str]: - return {k: v.default for k, v in self.config.items()} + if self.custom_config: + return self.custom_config + else: + return {k: v.default for k, v in self.config.items()} def set_config(self, data: Dict[str, str]) -> None: for key, value in data.items(): - config = self.config.get(key) - if config is None: + if key not in self.config: raise CoreError(f"unknown config: {key}") - config.default = value + self.custom_config[key] = value diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 2748cf85..8acf9475 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -297,6 +297,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) @@ -880,14 +892,11 @@ class CoreClient: 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_values = {} - for key, option in config.items(): - config_values[key] = option.value config_proto = configservices_pb2.ConfigServiceConfig( node_id=node_id, name=name, templates=service_config["templates"], - config=config_values, + config=config, ) config_service_protos.append(config_proto) return config_service_protos diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 8a4a0a0c..f92d23bb 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -93,15 +93,17 @@ class ConfigServiceConfigDialog(Dialog): node_configs = self.service_configs.get(self.node_id, {}) service_config = node_configs.get(self.service_name, {}) - self.default_config = response.config + 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: - self.config = custom_config - else: - self.config = dict(self.default_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) @@ -304,7 +306,7 @@ class ConfigServiceConfigDialog(Dialog): def click_apply(self): current_listbox = self.master.current.listbox - if not self.is_custom_service_config() and not self.is_custom_service_file(): + 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="") @@ -316,7 +318,9 @@ class ConfigServiceConfigDialog(Dialog): service_config = node_config.setdefault(self.service_name, {}) if self.config_frame: self.config_frame.parse_config() - service_config["config"] = self.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] @@ -346,18 +350,13 @@ class ConfigServiceConfigDialog(Dialog): else: self.modified_files.discard(template) - def is_custom_service_config(self): - startup_commands = self.startup_commands_listbox.get(0, "end") - shutdown_commands = self.shutdown_commands_listbox.get(0, "end") - validate_commands = self.validate_commands_listbox.get(0, "end") - return ( - set(self.default_startup) != set(startup_commands) - or set(self.default_validate) != set(validate_commands) - or set(self.default_shutdown) != set(shutdown_commands) - ) - - def is_custom_service_file(self): - return len(self.modified_files) > 0 + 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: @@ -368,8 +367,8 @@ class ConfigServiceConfigDialog(Dialog): self.template_text.text.delete(1.0, "end") self.template_text.text.insert("end", self.temp_service_files[filename]) if self.config_frame: - defaults = {x.id: x.value for x in self.default_config.values()} - self.config_frame.set_values(defaults) + logging.info("resetting defaults: %s", self.default_config) + self.config_frame.set_values(self.default_config) def click_copy(self): pass diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index bd9ec784..1230cede 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -153,14 +153,9 @@ class NodeConfigServiceDialog(Dialog): return def is_custom_service(self, service: str) -> bool: - service_configs = self.app.core.service_configs - file_configs = self.app.core.file_configs - if self.node_id in service_configs and service in service_configs[self.node_id]: + 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 - if ( - self.node_id in file_configs - and service in file_configs[self.node_id] - and file_configs[self.node_id][service] - ): - return True - return False + else: + return False diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index df73901f..4f51f4ce 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -9,7 +9,7 @@ from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import NodeTypes -from core.nodes.base import CoreNetworkBase, NodeBase +from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase from core.nodes.network import CtrlNet from core.services.coreservices import CoreService @@ -219,10 +219,15 @@ class DeviceElement(NodeElement): service_elements = etree.Element("services") for service in self.node.services: etree.SubElement(service_elements, "service", name=service.name) - if service_elements.getchildren(): self.element.append(service_elements) + config_service_elements = etree.Element("configservices") + for name, service in self.node.config_services.items(): + etree.SubElement(config_service_elements, "service", name=name) + if config_service_elements.getchildren(): + self.element.append(config_service_elements) + class NetworkElement(NodeElement): def __init__(self, session: "Session", node: NodeBase) -> None: @@ -261,6 +266,7 @@ class CoreXmlWriter: self.write_mobility_configs() self.write_emane_configs() self.write_service_configs() + self.write_configservice_configs() self.write_session_origin() self.write_session_hooks() self.write_session_options() @@ -399,6 +405,32 @@ class CoreXmlWriter: if service_configurations.getchildren(): self.scenario.append(service_configurations) + def write_configservice_configs(self) -> None: + service_configurations = etree.Element("configservice_configurations") + for node in self.session.nodes.values(): + if not isinstance(node, CoreNodeBase): + continue + for name, service in node.config_services.items(): + service_element = etree.SubElement( + service_configurations, "service", name=name + ) + add_attribute(service_element, "node", node.id) + if service.custom_config: + configs_element = etree.SubElement(service_element, "configs") + for key, value in service.custom_config.items(): + etree.SubElement( + configs_element, "config", key=key, value=value + ) + if service.custom_templates: + templates_element = etree.SubElement(service_element, "templates") + for template_name, template in service.custom_templates.items(): + template_element = etree.SubElement( + templates_element, "template", name=template_name + ) + template_element.text = etree.CDATA(template) + if service_configurations.getchildren(): + self.scenario.append(service_configurations) + def write_default_services(self) -> None: node_types = etree.Element("default_services") for node_type in self.session.services.default_services: @@ -568,6 +600,7 @@ class CoreXmlReader: self.read_mobility_configs() self.read_emane_configs() self.read_nodes() + self.read_configservice_configs() self.read_links() def read_default_services(self) -> None: @@ -768,6 +801,12 @@ class CoreXmlReader: if service_elements is not None: options.services = [x.get("name") for x in service_elements.iterchildren()] + config_service_elements = device_element.find("configservices") + if config_service_elements is not None: + options.config_services = [ + x.get("name") for x in config_service_elements.iterchildren() + ] + position_element = device_element.find("position") if position_element is not None: x = get_float(position_element, "x") @@ -808,6 +847,36 @@ class CoreXmlReader: ) self.session.add_node(_type=node_type, _id=node_id, options=options) + def read_configservice_configs(self) -> None: + configservice_configs = self.scenario.find("configservice_configurations") + if configservice_configs is None: + return + + for configservice_element in configservice_configs.iterchildren(): + name = configservice_element.get("name") + node_id = get_int(configservice_element, "node") + node = self.session.get_node(node_id) + service = node.config_services[name] + + configs_element = configservice_element.find("configs") + if configs_element is not None: + config = {} + for config_element in configs_element.iterchildren(): + key = config_element.get("key") + value = config_element.get("value") + config[key] = value + service.set_config(config) + + templates_element = configservice_element.find("templates") + if templates_element is not None: + for template_element in templates_element.iterchildren(): + name = template_element.get("name") + template = template_element.text + logging.info( + "loading xml template(%s): %s", type(template), template + ) + service.set_template(name, template) + def read_links(self) -> None: link_elements = self.scenario.find("links") if link_elements is None: diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto index e8d93fb0..f1272df8 100644 --- a/daemon/proto/core/api/grpc/configservices.proto +++ b/daemon/proto/core/api/grpc/configservices.proto @@ -57,6 +57,14 @@ message GetConfigServiceDefaultsResponse { repeated ConfigMode modes = 3; } +message GetNodeConfigServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeConfigServiceConfigsResponse { + repeated ConfigServiceConfig configs = 1; +} + message GetNodeConfigServiceRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index f7e9cf99..aa9bde24 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -110,6 +110,8 @@ service CoreApi { } rpc GetConfigServiceDefaults (configservices.GetConfigServiceDefaultsRequest) returns (configservices.GetConfigServiceDefaultsResponse) { } + rpc GetNodeConfigServiceConfigs (configservices.GetNodeConfigServiceConfigsRequest) returns (configservices.GetNodeConfigServiceConfigsResponse) { + } rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) { } rpc GetNodeConfigServices (configservices.GetNodeConfigServicesRequest) returns (configservices.GetNodeConfigServicesResponse) {