From 1074b32048be427d34bba7f13359aedad83fecf3 Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Mon, 25 Sep 2023 16:15:09 -0700
Subject: [PATCH 1/7] initial adjustments to remove legacy services

---
 daemon/core/api/grpc/client.py                | 106 +--
 daemon/core/api/grpc/grpcutils.py             | 103 +--
 daemon/core/api/grpc/server.py                | 129 ---
 daemon/core/api/grpc/wrappers.py              | 138 ----
 daemon/core/configservice/manager.py          |   7 +
 daemon/core/emulator/coreemu.py               |  11 -
 daemon/core/emulator/session.py               |   9 +-
 daemon/core/gui/coreclient.py                 |  64 --
 .../core/gui/dialogs/configserviceconfig.py   |   9 +-
 daemon/core/gui/dialogs/copyserviceconfig.py  | 119 ---
 daemon/core/gui/dialogs/nodeservice.py        | 154 ----
 daemon/core/gui/dialogs/serviceconfig.py      | 612 --------------
 daemon/core/gui/graph/node.py                 |   8 -
 daemon/core/nodes/base.py                     |  20 +-
 daemon/core/services/__init__.py              |   0
 daemon/core/services/bird.py                  | 233 ------
 daemon/core/services/coreservices.py          | 773 ------------------
 daemon/core/services/emaneservices.py         |  32 -
 daemon/core/services/frr.py                   | 683 ----------------
 daemon/core/services/nrl.py                   | 582 -------------
 daemon/core/services/quagga.py                | 584 -------------
 daemon/core/services/sdn.py                   | 131 ---
 daemon/core/services/security.py              | 164 ----
 daemon/core/services/ucarp.py                 | 165 ----
 daemon/core/services/utility.py               | 665 ---------------
 daemon/core/services/xorp.py                  | 436 ----------
 daemon/core/xml/corexml.py                    | 126 +--
 daemon/proto/core/api/grpc/core.proto         |  47 +-
 daemon/proto/core/api/grpc/services.proto     |  88 --
 daemon/tests/test_grpc.py                     | 114 +--
 daemon/tests/test_services.py                 | 376 ---------
 daemon/tests/test_xml.py                      |  16 +-
 32 files changed, 62 insertions(+), 6642 deletions(-)
 delete mode 100644 daemon/core/gui/dialogs/copyserviceconfig.py
 delete mode 100644 daemon/core/gui/dialogs/nodeservice.py
 delete mode 100644 daemon/core/gui/dialogs/serviceconfig.py
 delete mode 100644 daemon/core/services/__init__.py
 delete mode 100644 daemon/core/services/bird.py
 delete mode 100644 daemon/core/services/coreservices.py
 delete mode 100644 daemon/core/services/emaneservices.py
 delete mode 100644 daemon/core/services/frr.py
 delete mode 100644 daemon/core/services/nrl.py
 delete mode 100644 daemon/core/services/quagga.py
 delete mode 100644 daemon/core/services/sdn.py
 delete mode 100644 daemon/core/services/security.py
 delete mode 100644 daemon/core/services/ucarp.py
 delete mode 100644 daemon/core/services/utility.py
 delete mode 100644 daemon/core/services/xorp.py
 delete mode 100644 daemon/tests/test_services.py

diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index 4d6f02461..666230db4 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -38,14 +38,7 @@
     MobilityConfig,
     SetMobilityConfigRequest,
 )
-from core.api.grpc.services_pb2 import (
-    GetNodeServiceFileRequest,
-    GetNodeServiceRequest,
-    GetServiceDefaultsRequest,
-    ServiceActionRequest,
-    ServiceDefaults,
-    SetServiceDefaultsRequest,
-)
+from core.api.grpc.services_pb2 import ServiceActionRequest
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
     SetWlanConfigRequest,
@@ -725,103 +718,6 @@ def get_config(self) -> wrappers.CoreConfig:
         response = self.stub.GetConfig(request)
         return wrappers.CoreConfig.from_proto(response)
 
-    def get_service_defaults(self, session_id: int) -> list[wrappers.ServiceDefault]:
-        """
-        Get default services for different default node models.
-
-        :param session_id: session id
-        :return: list of service defaults
-        :raises grpc.RpcError: when session doesn't exist
-        """
-        request = GetServiceDefaultsRequest(session_id=session_id)
-        response = self.stub.GetServiceDefaults(request)
-        defaults = []
-        for default_proto in response.defaults:
-            default = wrappers.ServiceDefault.from_proto(default_proto)
-            defaults.append(default)
-        return defaults
-
-    def set_service_defaults(
-        self, session_id: int, service_defaults: dict[str, list[str]]
-    ) -> bool:
-        """
-        Set default services for node models.
-
-        :param session_id: session id
-        :param service_defaults: node models to lists of services
-        :return: True for success, False otherwise
-        :raises grpc.RpcError: when session doesn't exist
-        """
-        defaults = []
-        for model in service_defaults:
-            services = service_defaults[model]
-            default = ServiceDefaults(model=model, services=services)
-            defaults.append(default)
-        request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults)
-        response = self.stub.SetServiceDefaults(request)
-        return response.result
-
-    def get_node_service(
-        self, session_id: int, node_id: int, service: str
-    ) -> wrappers.NodeServiceData:
-        """
-        Get service data for a node.
-
-        :param session_id: session id
-        :param node_id: node id
-        :param service: service name
-        :return: node service data
-        :raises grpc.RpcError: when session or node doesn't exist
-        """
-        request = GetNodeServiceRequest(
-            session_id=session_id, node_id=node_id, service=service
-        )
-        response = self.stub.GetNodeService(request)
-        return wrappers.NodeServiceData.from_proto(response.service)
-
-    def get_node_service_file(
-        self, session_id: int, node_id: int, service: str, file_name: str
-    ) -> str:
-        """
-        Get a service file for a node.
-
-        :param session_id: session id
-        :param node_id: node id
-        :param service: service name
-        :param file_name: file name to get data for
-        :return: file data
-        :raises grpc.RpcError: when session or node doesn't exist
-        """
-        request = GetNodeServiceFileRequest(
-            session_id=session_id, node_id=node_id, service=service, file=file_name
-        )
-        response = self.stub.GetNodeServiceFile(request)
-        return response.data
-
-    def service_action(
-        self,
-        session_id: int,
-        node_id: int,
-        service: str,
-        action: wrappers.ServiceAction,
-    ) -> bool:
-        """
-        Send an action to a service for a node.
-
-        :param session_id: session id
-        :param node_id: node id
-        :param service: service name
-        :param action: action for service (start, stop, restart,
-            validate)
-        :return: True for success, False otherwise
-        :raises grpc.RpcError: when session or node doesn't exist
-        """
-        request = ServiceActionRequest(
-            session_id=session_id, node_id=node_id, service=service, action=action.value
-        )
-        response = self.stub.ServiceAction(request)
-        return response.result
-
     def config_service_action(
         self,
         session_id: int,
diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py
index 8d6b593d4..a2d779168 100644
--- a/daemon/core/api/grpc/grpcutils.py
+++ b/daemon/core/api/grpc/grpcutils.py
@@ -9,15 +9,9 @@
 from grpc import ServicerContext
 
 from core import utils
-from core.api.grpc import common_pb2, core_pb2, wrappers
+from core.api.grpc import common_pb2, core_pb2, services_pb2, wrappers
 from core.api.grpc.configservices_pb2 import ConfigServiceConfig
 from core.api.grpc.emane_pb2 import NodeEmaneConfig
-from core.api.grpc.services_pb2 import (
-    NodeServiceConfig,
-    NodeServiceData,
-    ServiceConfig,
-    ServiceDefaults,
-)
 from core.config import ConfigurableOptions
 from core.emane.nodes import EmaneNet, EmaneOptions
 from core.emulator.data import InterfaceData, LinkData, LinkOptions
@@ -40,7 +34,6 @@
 from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
 from core.nodes.podman import PodmanNode, PodmanOptions
 from core.nodes.wireless import WirelessNode
-from core.services.coreservices import CoreService
 
 logger = logging.getLogger(__name__)
 WORKERS = 10
@@ -80,7 +73,6 @@ def add_node_data(
     options.canvas = node_proto.canvas
     if isinstance(options, CoreNodeOptions):
         options.model = node_proto.model
-        options.services = node_proto.services
         options.config_services = node_proto.config_services
     if isinstance(options, EmaneOptions):
         options.emane_model = node_proto.emane
@@ -303,7 +295,6 @@ def get_node_proto(
     geo = core_pb2.Geo(
         lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
     )
-    services = [x.name for x in node.services]
     node_dir = None
     config_services = []
     if isinstance(node, CoreNodeBase):
@@ -345,18 +336,6 @@ def get_node_proto(
     )
     if mobility_config:
         mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility)
-    # check for service configs
-    custom_services = session.services.custom_services.get(node.id)
-    service_configs = {}
-    if custom_services:
-        for service in custom_services.values():
-            service_proto = get_service_configuration(service)
-            service_configs[service.name] = NodeServiceConfig(
-                node_id=node.id,
-                service=service.name,
-                data=service_proto,
-                files=service.config_data,
-            )
     # check for config service configs
     config_service_configs = {}
     if isinstance(node, CoreNode):
@@ -377,7 +356,6 @@ def get_node_proto(
         type=node_type.value,
         position=position,
         geo=geo,
-        services=services,
         icon=node.icon,
         image=image,
         config_services=config_services,
@@ -387,7 +365,6 @@ def get_node_proto(
         wlan_config=wlan_config,
         wireless_config=wireless_config,
         mobility_config=mobility_config,
-        service_configs=service_configs,
         config_service_configs=config_service_configs,
         emane_configs=emane_configs,
     )
@@ -626,49 +603,6 @@ def session_location(session: Session, location: core_pb2.SessionLocation) -> No
     session.location.refscale = location.scale
 
 
-def service_configuration(session: Session, config: ServiceConfig) -> None:
-    """
-    Convenience method for setting a node service configuration.
-
-    :param session: session for service configuration
-    :param config: service configuration
-    :return:
-    """
-    session.services.set_service(config.node_id, config.service)
-    service = session.services.get_service(config.node_id, config.service)
-    if config.files:
-        service.configs = tuple(config.files)
-    if config.directories:
-        service.dirs = tuple(config.directories)
-    if config.startup:
-        service.startup = tuple(config.startup)
-    if config.validate:
-        service.validate = tuple(config.validate)
-    if config.shutdown:
-        service.shutdown = tuple(config.shutdown)
-
-
-def get_service_configuration(service: CoreService) -> NodeServiceData:
-    """
-    Convenience for converting a service to service data proto.
-
-    :param service: service to get proto data for
-    :return: service proto data
-    """
-    return NodeServiceData(
-        executables=service.executables,
-        dependencies=service.dependencies,
-        dirs=service.dirs,
-        configs=service.configs,
-        startup=service.startup,
-        validate=service.validate,
-        validation_mode=service.validation_mode.value,
-        validation_timer=service.validation_timer,
-        shutdown=service.shutdown,
-        meta=service.meta,
-    )
-
-
 def iface_to_proto(session: Session, iface: CoreInterface) -> core_pb2.Interface:
     """
     Convenience for converting a core interface to the protobuf representation.
@@ -770,20 +704,6 @@ def get_hooks(session: Session) -> list[core_pb2.Hook]:
     return hooks
 
 
-def get_default_services(session: Session) -> list[ServiceDefaults]:
-    """
-    Retrieve the default service sets for a given session.
-
-    :param session: session to get default service sets for
-    :return: list of default service sets
-    """
-    default_services = []
-    for model, services in session.services.default_services.items():
-        default_service = ServiceDefaults(model=model, services=services)
-        default_services.append(default_service)
-    return default_services
-
-
 def get_mobility_node(
     session: Session, node_id: int, context: ServicerContext
 ) -> Union[WlanNode, EmaneNet]:
@@ -825,7 +745,6 @@ def convert_session(session: Session) -> wrappers.Session:
                 links.append(convert_link_data(link_data))
     for core_link in session.link_manager.links():
         links.extend(convert_core_link(core_link))
-    default_services = get_default_services(session)
     x, y, z = session.location.refxyz
     lat, lon, alt = session.location.refgeo
     location = core_pb2.SessionLocation(
@@ -838,6 +757,10 @@ def convert_session(session: Session) -> wrappers.Session:
         core_pb2.Server(name=x.name, host=x.host)
         for x in session.distributed.servers.values()
     ]
+    default_services = []
+    for group, services in session.service_manager.defaults.items():
+        defaults = services_pb2.ServiceDefaults(model=group, services=services)
+        default_services.append(defaults)
     return core_pb2.Session(
         id=session.id,
         state=session.state.value,
@@ -880,22 +803,6 @@ def configure_node(
     if isinstance(core_node, WirelessNode) and node.wireless_config:
         config = {k: v.value for k, v in node.wireless_config.items()}
         core_node.set_config(config)
-    for service_name, service_config in node.service_configs.items():
-        data = service_config.data
-        config = ServiceConfig(
-            node_id=node.id,
-            service=service_name,
-            startup=data.startup,
-            validate=data.validate,
-            shutdown=data.shutdown,
-            files=data.configs,
-            directories=data.dirs,
-        )
-        service_configuration(session, config)
-        for file_name, file_data in service_config.files.items():
-            session.services.set_service_file(
-                node.id, service_name, file_name, file_data
-            )
     if node.config_service_configs:
         if not isinstance(core_node, CoreNode):
             context.abort(
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index 0f48d38b4..8bb079210 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -67,18 +67,9 @@
     SetMobilityConfigResponse,
 )
 from core.api.grpc.services_pb2 import (
-    GetNodeServiceFileRequest,
-    GetNodeServiceFileResponse,
-    GetNodeServiceRequest,
-    GetNodeServiceResponse,
-    GetServiceDefaultsRequest,
-    GetServiceDefaultsResponse,
-    Service,
     ServiceAction,
     ServiceActionRequest,
     ServiceActionResponse,
-    SetServiceDefaultsRequest,
-    SetServiceDefaultsResponse,
 )
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
@@ -104,7 +95,6 @@
 from core.nodes.base import CoreNode, NodeBase
 from core.nodes.network import CoreNetwork, WlanNode
 from core.nodes.wireless import WirelessNode
-from core.services.coreservices import ServiceManager
 from core.xml.corexml import CoreXmlWriter
 
 logger = logging.getLogger(__name__)
@@ -231,11 +221,6 @@ def validate_service(
     def GetConfig(
         self, request: core_pb2.GetConfigRequest, context: ServicerContext
     ) -> core_pb2.GetConfigResponse:
-        services = []
-        for name in ServiceManager.services:
-            service = ServiceManager.services[name]
-            service_proto = Service(group=service.group, name=service.name)
-            services.append(service_proto)
         config_services = []
         for service in self.coreemu.service_manager.services.values():
             service_proto = configservices_pb2.ConfigService(
@@ -255,7 +240,6 @@ def GetConfig(
             config_services.append(service_proto)
         emane_models = [x.name for x in EmaneModelManager.models.values()]
         return core_pb2.GetConfigResponse(
-            services=services,
             config_services=config_services,
             emane_models=emane_models,
         )
@@ -922,119 +906,6 @@ def MobilityAction(
             result = False
         return MobilityActionResponse(result=result)
 
-    def GetServiceDefaults(
-        self, request: GetServiceDefaultsRequest, context: ServicerContext
-    ) -> GetServiceDefaultsResponse:
-        """
-        Retrieve all the default services of all node types in a session
-
-        :param request: get-default-service request
-        :param context: context object
-        :return: get-service-defaults response about all the available default services
-        """
-        logger.debug("get service defaults: %s", request)
-        session = self.get_session(request.session_id, context)
-        defaults = grpcutils.get_default_services(session)
-        return GetServiceDefaultsResponse(defaults=defaults)
-
-    def SetServiceDefaults(
-        self, request: SetServiceDefaultsRequest, context: ServicerContext
-    ) -> SetServiceDefaultsResponse:
-        """
-        Set new default services to the session after whipping out the old ones
-
-        :param request: set-service-defaults request
-        :param context: context object
-        :return: set-service-defaults response
-        """
-        logger.debug("set service defaults: %s", request)
-        session = self.get_session(request.session_id, context)
-        session.services.default_services.clear()
-        for service_defaults in request.defaults:
-            session.services.default_services[service_defaults.model] = list(
-                service_defaults.services
-            )
-        return SetServiceDefaultsResponse(result=True)
-
-    def GetNodeService(
-        self, request: GetNodeServiceRequest, context: ServicerContext
-    ) -> GetNodeServiceResponse:
-        """
-        Retrieve a requested service from a node
-
-        :param request: get-node-service
-            request
-        :param context: context object
-        :return: get-node-service response about the requested service
-        """
-        logger.debug("get node service: %s", request)
-        session = self.get_session(request.session_id, context)
-        service = session.services.get_service(
-            request.node_id, request.service, default_service=True
-        )
-        service_proto = grpcutils.get_service_configuration(service)
-        return GetNodeServiceResponse(service=service_proto)
-
-    def GetNodeServiceFile(
-        self, request: GetNodeServiceFileRequest, context: ServicerContext
-    ) -> GetNodeServiceFileResponse:
-        """
-        Retrieve a requested service file from a node
-
-        :param request:
-            get-node-service request
-        :param context: context object
-        :return: get-node-service response about the requested service
-        """
-        logger.debug("get node service file: %s", request)
-        session = self.get_session(request.session_id, context)
-        node = self.get_node(session, request.node_id, context, CoreNode)
-        file_data = session.services.get_service_file(
-            node, request.service, request.file
-        )
-        return GetNodeServiceFileResponse(data=file_data.data)
-
-    def ServiceAction(
-        self, request: ServiceActionRequest, context: ServicerContext
-    ) -> ServiceActionResponse:
-        """
-        Take action whether to start, stop, restart, validate the service or none of
-        the above.
-
-        :param request: service-action request
-        :param context: context object
-        :return: service-action response about status of action
-        """
-        logger.debug("service action: %s", request)
-        session = self.get_session(request.session_id, context)
-        node = self.get_node(session, request.node_id, context, CoreNode)
-        service = None
-        for current_service in node.services:
-            if current_service.name == request.service:
-                service = current_service
-                break
-
-        if not service:
-            context.abort(grpc.StatusCode.NOT_FOUND, "service not found")
-
-        status = -1
-        if request.action == ServiceAction.START:
-            status = session.services.startup_service(node, service, wait=True)
-        elif request.action == ServiceAction.STOP:
-            status = session.services.stop_service(node, service)
-        elif request.action == ServiceAction.RESTART:
-            status = session.services.stop_service(node, service)
-            if not status:
-                status = session.services.startup_service(node, service, wait=True)
-        elif request.action == ServiceAction.VALIDATE:
-            status = session.services.validate_service(node, service)
-
-        result = False
-        if not status:
-            result = True
-
-        return ServiceActionResponse(result=result)
-
     def ConfigServiceAction(
         self, request: ServiceActionRequest, context: ServicerContext
     ) -> ServiceActionResponse:
diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py
index cee87cd1b..cab6cc32b 100644
--- a/daemon/core/api/grpc/wrappers.py
+++ b/daemon/core/api/grpc/wrappers.py
@@ -199,16 +199,6 @@ def to_proto(self) -> core_pb2.Server:
         return core_pb2.Server(name=self.name, host=self.host)
 
 
-@dataclass
-class Service:
-    group: str
-    name: str
-
-    @classmethod
-    def from_proto(cls, proto: services_pb2.Service) -> "Service":
-        return Service(group=proto.group, name=proto.name)
-
-
 @dataclass
 class ServiceDefault:
     model: str
@@ -219,101 +209,6 @@ def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
         return ServiceDefault(model=proto.model, services=list(proto.services))
 
 
-@dataclass
-class NodeServiceData:
-    executables: list[str] = field(default_factory=list)
-    dependencies: list[str] = field(default_factory=list)
-    dirs: list[str] = field(default_factory=list)
-    configs: list[str] = field(default_factory=list)
-    startup: list[str] = field(default_factory=list)
-    validate: list[str] = field(default_factory=list)
-    validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING
-    validation_timer: int = 5
-    shutdown: list[str] = field(default_factory=list)
-    meta: str = None
-
-    @classmethod
-    def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData":
-        return NodeServiceData(
-            executables=list(proto.executables),
-            dependencies=list(proto.dependencies),
-            dirs=list(proto.dirs),
-            configs=list(proto.configs),
-            startup=list(proto.startup),
-            validate=list(proto.validate),
-            validation_mode=ServiceValidationMode(proto.validation_mode),
-            validation_timer=proto.validation_timer,
-            shutdown=list(proto.shutdown),
-            meta=proto.meta,
-        )
-
-    def to_proto(self) -> services_pb2.NodeServiceData:
-        return services_pb2.NodeServiceData(
-            executables=self.executables,
-            dependencies=self.dependencies,
-            dirs=self.dirs,
-            configs=self.configs,
-            startup=self.startup,
-            validate=self.validate,
-            validation_mode=self.validation_mode.value,
-            validation_timer=self.validation_timer,
-            shutdown=self.shutdown,
-            meta=self.meta,
-        )
-
-
-@dataclass
-class NodeServiceConfig:
-    node_id: int
-    service: str
-    data: NodeServiceData
-    files: dict[str, str] = field(default_factory=dict)
-
-    @classmethod
-    def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig":
-        return NodeServiceConfig(
-            node_id=proto.node_id,
-            service=proto.service,
-            data=NodeServiceData.from_proto(proto.data),
-            files=dict(proto.files),
-        )
-
-
-@dataclass
-class ServiceConfig:
-    node_id: int
-    service: str
-    files: list[str] = None
-    directories: list[str] = None
-    startup: list[str] = None
-    validate: list[str] = None
-    shutdown: list[str] = None
-
-    def to_proto(self) -> services_pb2.ServiceConfig:
-        return services_pb2.ServiceConfig(
-            node_id=self.node_id,
-            service=self.service,
-            files=self.files,
-            directories=self.directories,
-            startup=self.startup,
-            validate=self.validate,
-            shutdown=self.shutdown,
-        )
-
-
-@dataclass
-class ServiceFileConfig:
-    node_id: int
-    service: str
-    file: str
-    data: str = field(repr=False)
-
-    def to_proto(self) -> services_pb2.ServiceFileConfig:
-        return services_pb2.ServiceFileConfig(
-            node_id=self.node_id, service=self.service, file=self.file, data=self.data
-        )
-
-
 @dataclass
 class BridgeThroughput:
     node_id: int
@@ -723,7 +618,6 @@ class Node:
     type: NodeType = NodeType.DEFAULT
     model: str = None
     position: Position = Position(x=0, y=0)
-    services: set[str] = field(default_factory=set)
     config_services: set[str] = field(default_factory=set)
     emane: str = None
     icon: str = None
@@ -741,23 +635,12 @@ class Node:
     wlan_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
     wireless_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":
-        service_configs = {}
-        service_file_configs = {}
-        for service, node_config in proto.service_configs.items():
-            service_configs[service] = NodeServiceData.from_proto(node_config.data)
-            service_file_configs[service] = dict(node_config.files)
         emane_configs = {}
         for emane_config in proto.emane_configs:
             iface_id = None if emane_config.iface_id == -1 else emane_config.iface_id
@@ -776,7 +659,6 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             type=NodeType(proto.type),
             model=proto.model or None,
             position=Position.from_proto(proto.position),
-            services=set(proto.services),
             config_services=set(proto.config_services),
             emane=proto.emane,
             icon=proto.icon,
@@ -788,8 +670,6 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             canvas=proto.canvas,
             wlan_config=ConfigOption.from_dict(proto.wlan_config),
             mobility_config=ConfigOption.from_dict(proto.mobility_config),
-            service_configs=service_configs,
-            service_file_configs=service_file_configs,
             config_service_configs=config_service_configs,
             emane_model_configs=emane_configs,
             wireless_config=ConfigOption.from_dict(proto.wireless_config),
@@ -806,19 +686,6 @@ def to_proto(self) -> core_pb2.Node:
                 iface_id=iface_id, model=model, config=config
             )
             emane_configs.append(emane_config)
-        service_configs = {}
-        for service, service_data in self.service_configs.items():
-            service_configs[service] = services_pb2.NodeServiceConfig(
-                service=service, data=service_data.to_proto()
-            )
-        for service, file_configs in self.service_file_configs.items():
-            service_config = service_configs.get(service)
-            if service_config:
-                service_config.files.update(file_configs)
-            else:
-                service_configs[service] = services_pb2.NodeServiceConfig(
-                    service=service, files=file_configs
-                )
         config_service_configs = {}
         for service, service_config in self.config_service_configs.items():
             config_service_configs[service] = configservices_pb2.ConfigServiceConfig(
@@ -830,7 +697,6 @@ def to_proto(self) -> core_pb2.Node:
             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,
@@ -841,7 +707,6 @@ def to_proto(self) -> core_pb2.Node:
             canvas=self.canvas,
             wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()},
             mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()},
-            service_configs=service_configs,
             config_service_configs=config_service_configs,
             emane_configs=emane_configs,
             wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
@@ -993,16 +858,13 @@ def set_options(self, config: dict[str, str]) -> None:
 
 @dataclass
 class CoreConfig:
-    services: list[Service] = field(default_factory=list)
     config_services: list[ConfigService] = field(default_factory=list)
     emane_models: list[str] = field(default_factory=list)
 
     @classmethod
     def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
-        services = [Service.from_proto(x) for x in proto.services]
         config_services = [ConfigService.from_proto(x) for x in proto.config_services]
         return CoreConfig(
-            services=services,
             config_services=config_services,
             emane_models=list(proto.emane_models),
         )
diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py
index 542f3cc51..09b729851 100644
--- a/daemon/core/configservice/manager.py
+++ b/daemon/core/configservice/manager.py
@@ -20,6 +20,13 @@ def __init__(self):
         Create a ConfigServiceManager instance.
         """
         self.services: dict[str, type[ConfigService]] = {}
+        self.defaults: dict[str, list[str]] = {
+            "mdr": ["zebra", "OSPFv3MDR", "IPForward"],
+            "PC": ["DefaultRoute"],
+            "prouter": [],
+            "router": ["zebra", "OSPFv2", "OSPFv3", "IPForward"],
+            "host": ["DefaultRoute", "SSH"],
+        }
 
     def get_service(self, name: str) -> type[ConfigService]:
         """
diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py
index 574002e6f..9680523fe 100644
--- a/daemon/core/emulator/coreemu.py
+++ b/daemon/core/emulator/coreemu.py
@@ -7,7 +7,6 @@
 from core.emane.modelmanager import EmaneModelManager
 from core.emulator.session import Session
 from core.executables import get_requirements
-from core.services.coreservices import ServiceManager
 
 logger = logging.getLogger(__name__)
 
@@ -64,16 +63,6 @@ def _load_services(self) -> None:
 
         :return: nothing
         """
-        # load default services
-        self.service_errors = ServiceManager.load_locals()
-        # load custom services
-        service_paths = self.config.get("custom_services_dir")
-        logger.debug("custom service paths: %s", service_paths)
-        if service_paths is not None:
-            for service_path in service_paths.split(","):
-                service_path = Path(service_path.strip())
-                custom_service_errors = ServiceManager.add_services(service_path)
-                self.service_errors.extend(custom_service_errors)
         # load default config services
         self.service_manager.load_locals()
         # load custom config services
diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py
index a6804ea71..c0eda3682 100644
--- a/daemon/core/emulator/session.py
+++ b/daemon/core/emulator/session.py
@@ -58,7 +58,6 @@
 from core.nodes.podman import PodmanNode
 from core.nodes.wireless import WirelessNode
 from core.plugins.sdt import Sdt
-from core.services.coreservices import CoreServices
 from core.xml import corexml, corexmldeployment
 from core.xml.corexml import CoreXmlReader, CoreXmlWriter
 
@@ -152,7 +151,6 @@ def __init__(
         # initialize session feature helpers
         self.location: GeoLocation = GeoLocation()
         self.mobility: MobilityManager = MobilityManager(self)
-        self.services: CoreServices = CoreServices(self)
         self.emane: EmaneManager = EmaneManager(self)
         self.sdt: Sdt = Sdt(self)
 
@@ -606,7 +604,6 @@ def clear(self) -> None:
         self.emane.reset()
         self.emane.config_reset()
         self.location.reset()
-        self.services.reset()
         self.mobility.config_reset()
         self.link_colors.clear()
 
@@ -1055,8 +1052,6 @@ def data_collect(self) -> None:
             funcs = []
             for node in self.nodes.values():
                 if isinstance(node, CoreNodeBase) and node.up:
-                    args = (node,)
-                    funcs.append((self.services.stop_services, args, {}))
                     funcs.append((node.stop_config_services, (), {}))
             utils.threadpool(funcs)
 
@@ -1089,12 +1084,10 @@ def boot_node(self, node: CoreNode) -> None:
         :return: nothing
         """
         logger.info(
-            "booting node(%s): config services(%s) services(%s)",
+            "booting node(%s): config services(%s)",
             node.name,
             ", ".join(node.config_services.keys()),
-            ", ".join(x.name for x in node.services),
         )
-        self.services.boot_services(node)
         node.start_config_services()
 
     def boot_nodes(self) -> list[Exception]:
diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py
index d34eaa42d..e95aff08a 100644
--- a/daemon/core/gui/coreclient.py
+++ b/daemon/core/gui/coreclient.py
@@ -27,12 +27,9 @@
     MessageType,
     Node,
     NodeEvent,
-    NodeServiceData,
     NodeType,
     Position,
     Server,
-    ServiceConfig,
-    ServiceFileConfig,
     Session,
     SessionLocation,
     SessionState,
@@ -76,7 +73,6 @@ def __init__(self, app: "Application", proxy: bool) -> None:
         self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False)
 
         # global service settings
-        self.services: dict[str, set[str]] = {}
         self.config_services_groups: dict[str, set[str]] = {}
         self.config_services: dict[str, ConfigService] = {}
 
@@ -359,9 +355,6 @@ def setup(self, session_id: int = None) -> None:
             # get current core configurations services/config services
             core_config = self.client.get_config()
             self.emane_models = sorted(core_config.emane_models)
-            for service in core_config.services:
-                group_services = self.services.setdefault(service.group, set())
-                group_services.add(service.name)
             for service in core_config.config_services:
                 self.config_services[service.name] = service
                 group_services = self.config_services_groups.setdefault(
@@ -558,30 +551,6 @@ def open_xml(self, file_path: Path) -> None:
         except grpc.RpcError as e:
             self.app.show_grpc_exception("Open XML Error", e)
 
-    def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData:
-        node_service = self.client.get_node_service(
-            self.session.id, node_id, service_name
-        )
-        logger.debug(
-            "get node(%s) service(%s): %s", node_id, service_name, node_service
-        )
-        return node_service
-
-    def get_node_service_file(
-        self, node_id: int, service_name: str, file_name: str
-    ) -> str:
-        data = self.client.get_node_service_file(
-            self.session.id, node_id, service_name, file_name
-        )
-        logger.debug(
-            "get service file for node(%s), service: %s, file: %s, data: %s",
-            node_id,
-            service_name,
-            file_name,
-            data,
-        )
-        return data
-
     def close(self) -> None:
         """
         Clean ups when done using grpc
@@ -716,39 +685,6 @@ def get_emane_model_configs(self) -> list[EmaneModelConfig]:
                 configs.append(config)
         return configs
 
-    def get_service_configs(self) -> list[ServiceConfig]:
-        configs = []
-        for node in self.session.nodes.values():
-            if not nutils.is_container(node):
-                continue
-            if not node.service_configs:
-                continue
-            for name, config in node.service_configs.items():
-                config = ServiceConfig(
-                    node_id=node.id,
-                    service=name,
-                    files=config.configs,
-                    directories=config.dirs,
-                    startup=config.startup,
-                    validate=config.validate,
-                    shutdown=config.shutdown,
-                )
-                configs.append(config)
-        return configs
-
-    def get_service_file_configs(self) -> list[ServiceFileConfig]:
-        configs = []
-        for node in self.session.nodes.values():
-            if not nutils.is_container(node):
-                continue
-            if not node.service_file_configs:
-                continue
-            for service, file_configs in node.service_file_configs.items():
-                for file, data in file_configs.items():
-                    config = ServiceFileConfig(node.id, service, file, data)
-                    configs.append(config)
-        return configs
-
     def get_config_service_rendered(self, node_id: int, name: str) -> dict[str, str]:
         return self.client.get_config_service_rendered(self.session.id, node_id, name)
 
diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py
index 0e873a796..b78eabba7 100644
--- a/daemon/core/gui/dialogs/configserviceconfig.py
+++ b/daemon/core/gui/dialogs/configserviceconfig.py
@@ -317,16 +317,14 @@ def draw_tab_validation(self) -> None:
     def draw_buttons(self) -> None:
         frame = ttk.Frame(self.top)
         frame.grid(sticky=tk.EW)
-        for i in range(4):
+        for i in range(3):
             frame.columnconfigure(i, weight=1)
         button = ttk.Button(frame, text="Apply", command=self.click_apply)
         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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Copy...", command=self.click_copy)
-        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=tk.EW)
+        button.grid(row=0, column=2, sticky=tk.EW)
 
     def click_apply(self) -> None:
         current_listbox = self.master.current.listbox
@@ -403,9 +401,6 @@ def click_defaults(self) -> None:
             logger.info("resetting defaults: %s", self.default_config)
             self.config_frame.set_values(self.default_config)
 
-    def click_copy(self) -> None:
-        pass
-
     def append_commands(
         self, commands: list[str], listbox: tk.Listbox, to_add: list[str]
     ) -> None:
diff --git a/daemon/core/gui/dialogs/copyserviceconfig.py b/daemon/core/gui/dialogs/copyserviceconfig.py
deleted file mode 100644
index 6b2f4927e..000000000
--- a/daemon/core/gui/dialogs/copyserviceconfig.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-copy service config dialog
-"""
-
-import tkinter as tk
-from tkinter import ttk
-from typing import TYPE_CHECKING, Optional
-
-from core.gui.dialogs.dialog import Dialog
-from core.gui.themes import PADX, PADY
-from core.gui.widgets import CodeText, ListboxScroll
-
-if TYPE_CHECKING:
-    from core.gui.app import Application
-    from core.gui.dialogs.serviceconfig import ServiceConfigDialog
-
-
-class CopyServiceConfigDialog(Dialog):
-    def __init__(
-        self,
-        app: "Application",
-        dialog: "ServiceConfigDialog",
-        name: str,
-        service: str,
-        file_name: str,
-    ) -> None:
-        super().__init__(app, f"Copy Custom File to {name}", master=dialog)
-        self.dialog: "ServiceConfigDialog" = dialog
-        self.service: str = service
-        self.file_name: str = file_name
-        self.listbox: Optional[tk.Listbox] = None
-        self.nodes: dict[str, int] = {}
-        self.draw()
-
-    def draw(self) -> None:
-        self.top.columnconfigure(0, weight=1)
-        self.top.rowconfigure(1, weight=1)
-        label = ttk.Label(
-            self.top, text=f"{self.service} - {self.file_name}", anchor=tk.CENTER
-        )
-        label.grid(sticky=tk.EW, pady=PADY)
-
-        listbox_scroll = ListboxScroll(self.top)
-        listbox_scroll.grid(sticky=tk.NSEW, pady=PADY)
-        self.listbox = listbox_scroll.listbox
-        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
-            self.nodes[node.name] = node.id
-            self.listbox.insert(tk.END, node.name)
-
-        frame = ttk.Frame(self.top)
-        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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="View", command=self.click_view)
-        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=tk.EW)
-
-    def click_copy(self) -> None:
-        selection = self.listbox.curselection()
-        if not selection:
-            return
-        name = self.listbox.get(selection)
-        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)
-        self.dialog.service_file_data.text.insert(tk.END, data)
-        self.destroy()
-
-    def click_view(self) -> None:
-        selection = self.listbox.curselection()
-        if not selection:
-            return
-        name = self.listbox.get(selection)
-        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
-        )
-        dialog.show()
-
-
-class ViewConfigDialog(Dialog):
-    def __init__(
-        self,
-        app: "Application",
-        master: tk.BaseWidget,
-        name: str,
-        service: str,
-        file_name: str,
-        data: str,
-    ) -> None:
-        title = f"{name} Service({service}) File({file_name})"
-        super().__init__(app, title, master=master)
-        self.data = data
-        self.service_data = None
-        self.draw()
-
-    def draw(self) -> None:
-        self.top.columnconfigure(0, weight=1)
-        self.top.rowconfigure(0, weight=1)
-        self.service_data = CodeText(self.top)
-        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=tk.EW)
diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py
deleted file mode 100644
index 66e83fa44..000000000
--- a/daemon/core/gui/dialogs/nodeservice.py
+++ /dev/null
@@ -1,154 +0,0 @@
-"""
-core node services
-"""
-import tkinter as tk
-from tkinter import messagebox, ttk
-from typing import TYPE_CHECKING, Optional
-
-from core.api.grpc.wrappers import Node
-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
-
-if TYPE_CHECKING:
-    from core.gui.app import Application
-
-
-class NodeServiceDialog(Dialog):
-    def __init__(self, app: "Application", node: Node) -> None:
-        title = f"{node.name} Services (Deprecated)"
-        super().__init__(app, title)
-        self.node: Node = node
-        self.groups: Optional[ListboxScroll] = None
-        self.services: Optional[CheckboxList] = None
-        self.current: Optional[ListboxScroll] = None
-        services = set(node.services)
-        self.current_services: set[str] = services
-        self.protocol("WM_DELETE_WINDOW", self.click_cancel)
-        self.draw()
-
-    def draw(self) -> None:
-        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=tk.NSEW)
-        label_frame.rowconfigure(0, weight=1)
-        label_frame.columnconfigure(0, weight=1)
-        self.groups = ListboxScroll(label_frame)
-        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=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=tk.NSEW)
-
-        label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
-        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=tk.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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Save", command=self.click_save)
-        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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
-        button.grid(row=0, column=3, sticky=tk.EW)
-
-        # trigger group change
-        self.handle_group_change()
-
-    def handle_group_change(self, event: tk.Event = None) -> 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.services[group]):
-                checked = name in self.current_services
-                self.services.add(name, checked)
-
-    def service_clicked(self, name: str, var: tk.IntVar) -> None:
-        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.node.service_configs.pop(name, None)
-            self.node.service_file_configs.pop(name, None)
-        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.node.services = self.current_services.copy()
-
-    def click_configure(self) -> None:
-        current_selection = self.current.listbox.curselection()
-        if len(current_selection):
-            dialog = ServiceConfigDialog(
-                self,
-                self.app,
-                self.current.listbox.get(current_selection[0]),
-                self.node,
-            )
-
-            # if error occurred when creating ServiceConfigDialog, don't show the dialog
-            if not dialog.has_error:
-                dialog.show()
-            else:
-                dialog.destroy()
-        else:
-            messagebox.showinfo(
-                "Service Configuration", "Select a service to configure", parent=self
-            )
-
-    def click_cancel(self) -> None:
-        self.destroy()
-
-    def click_save(self) -> None:
-        self.node.services = self.current_services.copy()
-        self.destroy()
-
-    def click_remove(self) -> None:
-        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)
-            self.node.service_configs.pop(service, None)
-            self.node.service_file_configs.pop(service, None)
-            for checkbutton in self.services.frame.winfo_children():
-                if checkbutton["text"] == service:
-                    checkbutton.invoke()
-                    return
-
-    def is_custom_service(self, service: str) -> bool:
-        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
diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py
deleted file mode 100644
index 5eec7fafa..000000000
--- a/daemon/core/gui/dialogs/serviceconfig.py
+++ /dev/null
@@ -1,612 +0,0 @@
-import logging
-import tkinter as tk
-from pathlib import Path
-from tkinter import filedialog, messagebox, ttk
-from typing import TYPE_CHECKING, Optional
-
-import grpc
-from PIL.ImageTk import PhotoImage
-
-from core.api.grpc.wrappers import Node, NodeServiceData, ServiceValidationMode
-from core.gui import images
-from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog
-from core.gui.dialogs.dialog import Dialog
-from core.gui.images import ImageEnum
-from core.gui.themes import FRAME_PAD, PADX, PADY
-from core.gui.widgets import CodeText, ListboxScroll
-
-logger = logging.getLogger(__name__)
-
-if TYPE_CHECKING:
-    from core.gui.app import Application
-    from core.gui.coreclient import CoreClient
-
-ICON_SIZE: int = 16
-
-
-class ServiceConfigDialog(Dialog):
-    def __init__(
-        self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
-    ) -> None:
-        title = f"{service_name} Service (Deprecated)"
-        super().__init__(app, title, master=master)
-        self.core: "CoreClient" = app.core
-        self.node: Node = node
-        self.service_name: str = service_name
-        self.radiovar: tk.IntVar = tk.IntVar(value=2)
-        self.metadata: str = ""
-        self.filenames: list[str] = []
-        self.dependencies: list[str] = []
-        self.executables: list[str] = []
-        self.startup_commands: list[str] = []
-        self.validation_commands: list[str] = []
-        self.shutdown_commands: list[str] = []
-        self.default_startup: list[str] = []
-        self.default_validate: list[str] = []
-        self.default_shutdown: list[str] = []
-        self.validation_mode: Optional[ServiceValidationMode] = None
-        self.validation_time: Optional[int] = None
-        self.validation_period: Optional[float] = None
-        self.directory_entry: Optional[ttk.Entry] = None
-        self.default_directories: list[str] = []
-        self.temp_directories: list[str] = []
-        self.documentnew_img: PhotoImage = self.app.get_enum_icon(
-            ImageEnum.DOCUMENTNEW, width=ICON_SIZE
-        )
-        self.editdelete_img: PhotoImage = self.app.get_enum_icon(
-            ImageEnum.EDITDELETE, width=ICON_SIZE
-        )
-        self.notebook: Optional[ttk.Notebook] = None
-        self.metadata_entry: Optional[ttk.Entry] = None
-        self.filename_combobox: Optional[ttk.Combobox] = None
-        self.dir_list: Optional[ListboxScroll] = None
-        self.startup_commands_listbox: Optional[tk.Listbox] = None
-        self.shutdown_commands_listbox: Optional[tk.Listbox] = None
-        self.validate_commands_listbox: Optional[tk.Listbox] = None
-        self.validation_time_entry: Optional[ttk.Entry] = None
-        self.validation_mode_entry: Optional[ttk.Entry] = None
-        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: Optional[NodeServiceData] = None
-        self.temp_service_files: dict[str, str] = {}
-        self.modified_files: set[str] = set()
-        self.has_error: bool = False
-        self.load()
-        if not self.has_error:
-            self.draw()
-
-    def load(self) -> None:
-        try:
-            self.core.start_session(definition=True)
-            default_config = self.app.core.get_node_service(
-                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.node.service_configs.get(self.service_name)
-            self.default_config = default_config
-            service_config = (
-                custom_service_config if custom_service_config else default_config
-            )
-            self.dependencies = service_config.dependencies[:]
-            self.executables = service_config.executables[:]
-            self.metadata = service_config.meta
-            self.filenames = service_config.configs[:]
-            self.startup_commands = service_config.startup[:]
-            self.validation_commands = service_config.validate[:]
-            self.shutdown_commands = service_config.shutdown[:]
-            self.validation_mode = service_config.validation_mode
-            self.validation_time = service_config.validation_timer
-            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
-                )
-                for x in default_config.configs
-            }
-            self.temp_service_files = dict(self.original_service_files)
-
-            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:
-            self.app.show_grpc_exception("Get Node Service Error", e)
-            self.has_error = True
-
-    def draw(self) -> None:
-        self.top.columnconfigure(0, weight=1)
-        self.top.rowconfigure(1, weight=1)
-
-        # draw metadata
-        frame = ttk.Frame(self.top)
-        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=tk.W, padx=PADX)
-        self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata)
-        self.metadata_entry.grid(row=0, column=1, sticky=tk.EW)
-
-        # draw notebook
-        self.notebook = ttk.Notebook(self.top)
-        self.notebook.grid(sticky=tk.NSEW, pady=PADY)
-        self.draw_tab_files()
-        self.draw_tab_directories()
-        self.draw_tab_startstop()
-        self.draw_tab_configuration()
-
-        self.draw_buttons()
-
-    def draw_tab_files(self) -> None:
-        tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
-        tab.grid(sticky=tk.NSEW)
-        tab.columnconfigure(0, weight=1)
-        self.notebook.add(tab, text="Files")
-
-        label = ttk.Label(
-            tab, text="Config files and scripts that are generated for this service."
-        )
-        label.grid()
-
-        frame = ttk.Frame(tab)
-        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=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=tk.EW, padx=PADX)
-        button = ttk.Button(
-            frame, image=self.documentnew_img, command=self.add_filename
-        )
-        button.grid(row=0, column=2, padx=PADX)
-        button = ttk.Button(
-            frame, image=self.editdelete_img, command=self.delete_filename
-        )
-        button.grid(row=0, column=3)
-
-        frame = ttk.Frame(tab)
-        frame.grid(sticky=tk.EW, pady=PADY)
-        frame.columnconfigure(1, weight=1)
-        button = ttk.Radiobutton(
-            frame,
-            variable=self.radiovar,
-            text="Copy Source File",
-            value=1,
-            state=tk.DISABLED,
-        )
-        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=tk.EW, padx=PADX)
-        image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
-        button = ttk.Button(frame, image=image)
-        button.image = image
-        button.grid(row=0, column=2)
-
-        frame = ttk.Frame(tab)
-        frame.grid(sticky=tk.EW, pady=PADY)
-        frame.columnconfigure(0, weight=1)
-        button = ttk.Radiobutton(
-            frame,
-            variable=self.radiovar,
-            text="Use text below for file contents",
-            value=2,
-        )
-        button.grid(row=0, column=0, sticky=tk.EW)
-        image = images.from_enum(ImageEnum.FILEOPEN, width=images.BUTTON_SIZE)
-        button = ttk.Button(frame, image=image)
-        button.image = image
-        button.grid(row=0, column=1)
-        image = images.from_enum(ImageEnum.DOCUMENTSAVE, width=images.BUTTON_SIZE)
-        button = ttk.Button(frame, image=image)
-        button.image = image
-        button.grid(row=0, column=2)
-
-        self.service_file_data = CodeText(tab)
-        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)
-            self.service_file_data.text.delete(1.0, "end")
-            self.service_file_data.text.insert(
-                "end", self.temp_service_files[self.filenames[0]]
-            )
-        self.service_file_data.text.bind(
-            "<FocusOut>", self.update_temp_service_file_data
-        )
-
-    def draw_tab_directories(self) -> None:
-        tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
-        tab.grid(sticky=tk.NSEW)
-        tab.columnconfigure(0, weight=1)
-        tab.rowconfigure(2, weight=1)
-        self.notebook.add(tab, text="Directories")
-
-        label = ttk.Label(
-            tab,
-            text="Directories required by this service that are unique for each node.",
-        )
-        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=tk.NSEW)
-        var = tk.StringVar(value="")
-        self.directory_entry = ttk.Entry(frame, textvariable=var)
-        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=tk.EW)
-        self.dir_list = ListboxScroll(tab)
-        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=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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Remove", command=self.remove_directory)
-        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=tk.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(1, weight=1)
-            label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY)
-
-            frame = ttk.Frame(label_frame)
-            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=tk.EW, padx=PADX)
-            button = ttk.Button(frame, image=self.editdelete_img)
-            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=tk.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_configuration(self) -> None:
-        tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
-        tab.grid(sticky=tk.NSEW)
-        tab.columnconfigure(0, weight=1)
-        self.notebook.add(tab, text="Configuration", sticky=tk.NSEW)
-
-        frame = ttk.Frame(tab)
-        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=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=tk.EW, pady=PADY)
-
-        label = ttk.Label(frame, text="Validation Mode")
-        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:
-            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=tk.EW, pady=PADY)
-
-        label = ttk.Label(frame, text="Validation Period")
-        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=tk.EW, pady=PADY)
-
-        label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
-        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=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=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=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=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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
-        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=tk.EW, padx=PADX)
-        button = ttk.Button(frame, text="Cancel", command=self.destroy)
-        button.grid(row=0, column=3, sticky=tk.EW)
-
-    def add_filename(self) -> None:
-        filename = self.filename_combobox.get()
-        if filename not in self.filename_combobox["values"]:
-            self.filename_combobox["values"] += (filename,)
-            self.filename_combobox.set(filename)
-            self.temp_service_files[filename] = self.service_file_data.text.get(
-                1.0, "end"
-            )
-        else:
-            logger.debug("file already existed")
-
-    def delete_filename(self) -> None:
-        cbb = self.filename_combobox
-        filename = cbb.get()
-        if filename in cbb["values"]:
-            cbb["values"] = tuple([x for x in cbb["values"] if x != filename])
-        cbb.set("")
-        self.service_file_data.text.delete(1.0, "end")
-        self.temp_service_files.pop(filename, None)
-        if filename in self.modified_files:
-            self.modified_files.remove(filename)
-
-    @classmethod
-    def add_command(cls, event: tk.Event) -> None:
-        frame_contains_button = event.widget.master
-        listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
-        command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get()
-        if command_to_add == "":
-            return
-        for cmd in listbox.get(0, tk.END):
-            if cmd == command_to_add:
-                return
-        listbox.insert(tk.END, command_to_add)
-
-    @classmethod
-    def update_entry(cls, event: tk.Event) -> None:
-        listbox = event.widget
-        current_selection = listbox.curselection()
-        if len(current_selection) > 0:
-            cmd = listbox.get(current_selection[0])
-            entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves(
-                row=0, column=0
-            )[0]
-            entry.delete(0, "end")
-            entry.insert(0, cmd)
-
-    @classmethod
-    def delete_command(cls, event: tk.Event) -> None:
-        button = event.widget
-        frame_contains_button = button.master
-        listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox
-        current_selection = listbox.curselection()
-        if len(current_selection) > 0:
-            listbox.delete(current_selection[0])
-            entry = frame_contains_button.grid_slaves(row=0, column=0)[0]
-            entry.delete(0, tk.END)
-
-    def click_apply(self) -> None:
-        if (
-            not self.is_custom_command()
-            and not self.is_custom_service_file()
-            and not self.has_new_files()
-            and not self.is_custom_directory()
-        ):
-            self.node.service_configs.pop(self.service_name, None)
-            self.current_service_color("")
-            self.destroy()
-            return
-        files = set(self.filenames)
-        if (
-            self.is_custom_command()
-            or self.has_new_files()
-            or self.is_custom_directory()
-        ):
-            startup, validate, shutdown = self.get_commands()
-            files = set(self.filename_combobox["values"])
-            service_data = NodeServiceData(
-                configs=list(files),
-                dirs=self.temp_directories,
-                startup=startup,
-                validate=validate,
-                shutdown=shutdown,
-            )
-            logger.info("setting service data: %s", service_data)
-            self.node.service_configs[self.service_name] = service_data
-        for file in self.modified_files:
-            if file not in files:
-                continue
-            file_configs = self.node.service_file_configs.setdefault(
-                self.service_name, {}
-            )
-            file_configs[file] = self.temp_service_files[file]
-        self.current_service_color("green")
-        self.destroy()
-
-    def display_service_file_data(self, event: tk.Event) -> None:
-        filename = self.filename_combobox.get()
-        self.service_file_data.text.delete(1.0, "end")
-        self.service_file_data.text.insert("end", self.temp_service_files[filename])
-
-    def update_temp_service_file_data(self, event: tk.Event) -> None:
-        filename = self.filename_combobox.get()
-        self.temp_service_files[filename] = self.service_file_data.text.get(1.0, "end")
-        if self.temp_service_files[filename] != self.original_service_files.get(
-            filename, ""
-        ):
-            self.modified_files.add(filename)
-        else:
-            self.modified_files.discard(filename)
-
-    def is_custom_command(self) -> bool:
-        startup, validate, shutdown = self.get_commands()
-        return (
-            set(self.default_startup) != set(startup)
-            or set(self.default_validate) != set(validate)
-            or set(self.default_shutdown) != set(shutdown)
-        )
-
-    def has_new_files(self) -> bool:
-        return set(self.filenames) != set(self.filename_combobox["values"])
-
-    def is_custom_service_file(self) -> bool:
-        return len(self.modified_files) > 0
-
-    def is_custom_directory(self) -> bool:
-        return set(self.default_directories) != set(self.dir_list.listbox.get(0, "end"))
-
-    def click_defaults(self) -> None:
-        """
-        clears out any custom configuration permanently
-        """
-        # clear coreclient data
-        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()
-
-        # reset files tab
-        files = list(self.default_config.configs[:])
-        self.filenames = files
-        self.filename_combobox.config(values=files)
-        self.service_file_data.text.delete(1.0, "end")
-        if len(files) > 0:
-            filename = files[0]
-            self.filename_combobox.set(filename)
-            self.service_file_data.text.insert("end", self.temp_service_files[filename])
-
-        # reset commands
-        self.startup_commands_listbox.delete(0, tk.END)
-        self.validate_commands_listbox.delete(0, tk.END)
-        self.shutdown_commands_listbox.delete(0, tk.END)
-        for cmd in self.default_startup:
-            self.startup_commands_listbox.insert(tk.END, cmd)
-        for cmd in self.default_validate:
-            self.validate_commands_listbox.insert(tk.END, cmd)
-        for cmd in self.default_shutdown:
-            self.shutdown_commands_listbox.insert(tk.END, cmd)
-
-        # reset directories
-        self.directory_entry.delete(0, "end")
-        self.dir_list.listbox.delete(0, "end")
-        self.temp_directories = list(self.default_directories)
-        for d in self.default_directories:
-            self.dir_list.listbox.insert("end", d)
-
-        self.current_service_color("")
-
-    def click_copy(self) -> None:
-        file_name = self.filename_combobox.get()
-        dialog = CopyServiceConfigDialog(
-            self.app, self, self.node.name, self.service_name, file_name
-        )
-        dialog.show()
-
-    @classmethod
-    def append_commands(
-        cls, commands: list[str], listbox: tk.Listbox, to_add: list[str]
-    ) -> None:
-        for cmd in to_add:
-            commands.append(cmd)
-            listbox.insert(tk.END, cmd)
-
-    def get_commands(self) -> tuple[list[str], list[str], list[str]]:
-        startup = self.startup_commands_listbox.get(0, "end")
-        shutdown = self.shutdown_commands_listbox.get(0, "end")
-        validate = self.validate_commands_listbox.get(0, "end")
-        return startup, validate, shutdown
-
-    def find_directory_button(self) -> None:
-        d = filedialog.askdirectory(initialdir="/")
-        self.directory_entry.delete(0, "end")
-        self.directory_entry.insert("end", d)
-
-    def add_directory(self) -> None:
-        directory = Path(self.directory_entry.get())
-        if directory.is_absolute():
-            if str(directory) not in self.temp_directories:
-                self.dir_list.listbox.insert("end", directory)
-                self.temp_directories.append(str(directory))
-        else:
-            messagebox.showerror("Add Directory", "Path must be absolute!", parent=self)
-
-    def remove_directory(self) -> None:
-        d = self.directory_entry.get()
-        dirs = self.dir_list.listbox.get(0, "end")
-        if d and d in self.temp_directories:
-            self.temp_directories.remove(d)
-            try:
-                i = dirs.index(d)
-                self.dir_list.listbox.delete(i)
-            except ValueError:
-                logger.debug("directory is not in the list")
-        self.directory_entry.delete(0, "end")
-
-    def directory_select(self, event) -> None:
-        i = self.dir_list.listbox.curselection()
-        if i:
-            d = self.dir_list.listbox.get(i)
-            self.directory_entry.delete(0, "end")
-            self.directory_entry.insert("end", d)
-
-    def current_service_color(self, color="") -> None:
-        """
-        change the current service label color
-        """
-        listbox = self.master.current.listbox
-        services = listbox.get(0, tk.END)
-        listbox.itemconfig(services.index(self.service_name), bg=color)
diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py
index 0cfbf2e97..b63f4bf5b 100644
--- a/daemon/core/gui/graph/node.py
+++ b/daemon/core/gui/graph/node.py
@@ -15,7 +15,6 @@
 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.wirelessconfig import WirelessConfigDialog
 from core.gui.dialogs.wlanconfig import WlanConfigDialog
 from core.gui.frames.node import NodeInfoFrame
@@ -263,9 +262,6 @@ def show_context(self, event: tk.Event) -> None:
                 self.context.add_command(
                     label="Config Services", command=self.show_config_services
                 )
-                self.context.add_command(
-                    label="Services (Deprecated)", command=self.show_services
-                )
             if is_emane:
                 self.context.add_command(
                     label="EMANE Config", command=self.show_emane_config
@@ -378,10 +374,6 @@ def show_emane_config(self) -> None:
         dialog = EmaneConfigDialog(self.app, self.core_node)
         dialog.show()
 
-    def show_services(self) -> None:
-        dialog = NodeServiceDialog(self.app, self.core_node)
-        dialog.show()
-
     def show_config_services(self) -> None:
         dialog = NodeConfigServiceDialog(self.app, self.core_node)
         dialog.show()
diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py
index e59a89e40..d7074c88d 100644
--- a/daemon/core/nodes/base.py
+++ b/daemon/core/nodes/base.py
@@ -9,7 +9,7 @@
 from dataclasses import dataclass, field
 from pathlib import Path
 from threading import RLock
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING, Optional
 
 import netaddr
 
@@ -27,9 +27,7 @@
     from core.emulator.distributed import DistributedServer
     from core.emulator.session import Session
     from core.configservice.base import ConfigService
-    from core.services.coreservices import CoreService
 
-    CoreServices = list[Union[CoreService, type[CoreService]]]
     ConfigServiceType = type[ConfigService]
 
 PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")]
@@ -151,7 +149,6 @@ def __init__(
         self.name: str = name or f"{self.__class__.__name__}{self.id}"
         self.server: "DistributedServer" = server
         self.model: Optional[str] = None
-        self.services: CoreServices = []
         self.ifaces: dict[int, CoreInterface] = {}
         self.iface_id: int = 0
         self.position: Position = Position()
@@ -589,15 +586,16 @@ def __init__(
         )
         options = options or CoreNodeOptions()
         self.model: Optional[str] = options.model
-        # setup services
-        if options.legacy or options.services:
-            logger.debug("set node type: %s", self.model)
-            self.session.services.add_services(self, self.model, options.services)
         # add config services
         config_services = options.config_services
-        if not options.legacy and not config_services and not options.services:
-            config_services = self.session.services.default_services.get(self.model, [])
-        logger.info("setting node config services: %s", config_services)
+        if not config_services:
+            config_services = self.session.service_manager.defaults.get(self.model, [])
+        logger.info(
+            "setting node(%s) model(%s) config services: %s",
+            self.name,
+            self.model,
+            config_services,
+        )
         for name in config_services:
             service_class = self.session.service_manager.get_service(name)
             self.add_config_service(service_class)
diff --git a/daemon/core/services/__init__.py b/daemon/core/services/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py
deleted file mode 100644
index c2ecc4dcc..000000000
--- a/daemon/core/services/bird.py
+++ /dev/null
@@ -1,233 +0,0 @@
-"""
-bird.py: defines routing services provided by the BIRD Internet Routing Daemon.
-"""
-from typing import Optional
-
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService
-
-
-class Bird(CoreService):
-    """
-    Bird router support
-    """
-
-    name: str = "bird"
-    group: str = "BIRD"
-    executables: tuple[str, ...] = ("bird",)
-    dirs: tuple[str, ...] = ("/etc/bird",)
-    configs: tuple[str, ...] = ("/etc/bird/bird.conf",)
-    startup: tuple[str, ...] = (f"bird -c {configs[0]}",)
-    shutdown: tuple[str, ...] = ("killall bird",)
-    validate: tuple[str, ...] = ("pidof bird",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the bird.conf file contents.
-        """
-        if filename == cls.configs[0]:
-            return cls.generate_bird_config(node)
-        else:
-            raise ValueError
-
-    @staticmethod
-    def router_id(node: CoreNode) -> str:
-        """
-        Helper to return the first IPv4 address of a node as its router ID.
-        """
-        for iface in node.get_ifaces(control=False):
-            ip4 = iface.get_ip4()
-            if ip4:
-                return str(ip4.ip)
-        return "0.0.0.0"
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text. Other services that depend on bird
-        will have hooks that are invoked here.
-        """
-        cfg = f"""\
-/* Main configuration file for BIRD. This is ony a template,
- * you will *need* to customize it according to your needs
- * Beware that only double quotes \'"\' are valid. No singles. */
-
-
-log "/var/log/{cls.name}.log" all;
-#debug protocols all;
-#debug commands 2;
-
-router id  {cls.router_id(node)};       # Mandatory for IPv6, may be automatic for IPv4
-
-protocol kernel {{
-    persist;                # Don\'t remove routes on BIRD shutdown
-    scan time 200;          # Scan kernel routing table every 200 seconds
-    export all;
-    import all;
-}}
-
-protocol device {{
-    scan time 10;           # Scan interfaces every 10 seconds
-}}
-
-"""
-
-        # generate protocol specific configurations
-        for s in node.services:
-            if cls.name not in s.dependencies:
-                continue
-            if not (isinstance(s, BirdService) or issubclass(s, BirdService)):
-                continue
-            cfg += s.generate_bird_config(node)
-        return cfg
-
-
-class BirdService(CoreService):
-    """
-    Parent class for Bird services. Defines properties and methods
-    common to Bird's routing daemons.
-    """
-
-    name: Optional[str] = None
-    group: str = "BIRD"
-    executables: tuple[str, ...] = ("bird",)
-    dependencies: tuple[str, ...] = ("bird",)
-    meta: str = "The config file for this service can be found in the bird service."
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        return ""
-
-    @classmethod
-    def generate_bird_iface_config(cls, node: CoreNode) -> str:
-        """
-        Use only bare interfaces descriptions in generated protocol
-        configurations. This has the slight advantage of being the same
-        everywhere.
-        """
-        cfg = ""
-        for iface in node.get_ifaces(control=False):
-            cfg += f'        interface "{iface.name}";\n'
-        return cfg
-
-
-class BirdBgp(BirdService):
-    """
-    BGP BIRD Service (configuration generation)
-    """
-
-    name: str = "BIRD_BGP"
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        return """
-/* This is a sample config that should be customized with appropriate AS numbers
- * and peers; add one section like this for each neighbor */
-
-protocol bgp {
-    local as 65000;                      # Customize your AS number
-    neighbor 198.51.100.130 as 64496;    # Customize neighbor AS number && IP
-    export filter {                      # We use non-trivial export rules
-        # This is an example. You should advertise only *your routes*
-        if (source = RTS_DEVICE) || (source = RTS_OSPF) then {
-#           bgp_community.add((65000,64501)); # Assign our community
-            accept;
-        }
-        reject;
-    };
-    import all;
-}
-
-"""
-
-
-class BirdOspf(BirdService):
-    """
-    OSPF BIRD Service (configuration generation)
-    """
-
-    name: str = "BIRD_OSPFv2"
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        cfg = "protocol ospf {\n"
-        cfg += "    export filter {\n"
-        cfg += "        if source = RTS_BGP then {\n"
-        cfg += "            ospf_metric1 = 100;\n"
-        cfg += "            accept;\n"
-        cfg += "        }\n"
-        cfg += "        accept;\n"
-        cfg += "    };\n"
-        cfg += "    area 0.0.0.0 {\n"
-        cfg += cls.generate_bird_iface_config(node)
-        cfg += "    };\n"
-        cfg += "}\n\n"
-        return cfg
-
-
-class BirdRadv(BirdService):
-    """
-    RADV BIRD Service (configuration generation)
-    """
-
-    name: str = "BIRD_RADV"
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        cfg = "/* This is a sample config that must be customized */\n"
-        cfg += "protocol radv {\n"
-        cfg += "    # auto configuration on all interfaces\n"
-        cfg += cls.generate_bird_iface_config(node)
-        cfg += "    # Advertise DNS\n"
-        cfg += "    rdnss {\n"
-        cfg += "#        lifetime mult 10;\n"
-        cfg += "#        lifetime mult 10;\n"
-        cfg += "#        ns 2001:0DB8:1234::11;\n"
-        cfg += "#        ns 2001:0DB8:1234::11;\n"
-        cfg += "#        ns 2001:0DB8:1234::12;\n"
-        cfg += "#        ns 2001:0DB8:1234::12;\n"
-        cfg += "    };\n"
-        cfg += "}\n\n"
-        return cfg
-
-
-class BirdRip(BirdService):
-    """
-    RIP BIRD Service (configuration generation)
-    """
-
-    name: str = "BIRD_RIP"
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        cfg = "protocol rip {\n"
-        cfg += "    period 10;\n"
-        cfg += "    garbage time 60;\n"
-        cfg += cls.generate_bird_iface_config(node)
-        cfg += "    honor neighbor;\n"
-        cfg += "    authentication none;\n"
-        cfg += "    import all;\n"
-        cfg += "    export all;\n"
-        cfg += "}\n\n"
-        return cfg
-
-
-class BirdStatic(BirdService):
-    """
-    Static Bird Service (configuration generation)
-    """
-
-    name: str = "BIRD_static"
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_bird_config(cls, node: CoreNode) -> str:
-        cfg = "/* This is a sample config that must be customized */\n"
-        cfg += "protocol static {\n"
-        cfg += "#    route 0.0.0.0/0 via 198.51.100.130; # Default route. Do NOT advertise on BGP !\n"
-        cfg += "#    route 203.0.113.0/24 reject;        # Sink route\n"
-        cfg += '#    route 10.2.0.0/24 via "arc0";       # Secondary network\n'
-        cfg += "}\n\n"
-        return cfg
diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py
deleted file mode 100644
index 0eee980ed..000000000
--- a/daemon/core/services/coreservices.py
+++ /dev/null
@@ -1,773 +0,0 @@
-"""
-Definition of CoreService class that is subclassed to define
-startup services and routing for nodes. A service is typically a daemon
-program launched when a node starts that provides some sort of service.
-The CoreServices class handles configuration messages for sending
-a list of available services to the GUI and for configuring individual
-services.
-"""
-
-import enum
-import logging
-import pkgutil
-import time
-from collections.abc import Iterable
-from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
-
-from core import services as core_services
-from core import utils
-from core.emulator.data import FileData
-from core.emulator.enumerations import ExceptionLevels, MessageFlags, RegisterTlvs
-from core.errors import (
-    CoreCommandError,
-    CoreError,
-    CoreServiceBootError,
-    CoreServiceError,
-)
-from core.nodes.base import CoreNode
-
-logger = logging.getLogger(__name__)
-
-if TYPE_CHECKING:
-    from core.emulator.session import Session
-
-    CoreServiceType = Union["CoreService", type["CoreService"]]
-
-
-class ServiceMode(enum.Enum):
-    BLOCKING = 0
-    NON_BLOCKING = 1
-    TIMER = 2
-
-
-class ServiceDependencies:
-    """
-    Can generate boot paths for services, based on their dependencies. Will validate
-    that all services will be booted and that all dependencies exist within the services
-    provided.
-    """
-
-    def __init__(self, services: list["CoreServiceType"]) -> None:
-        self.visited: set[str] = set()
-        self.services: dict[str, "CoreServiceType"] = {}
-        self.paths: dict[str, list["CoreServiceType"]] = {}
-        self.boot_paths: list[list["CoreServiceType"]] = []
-        roots = {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 _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
-
-    def boot_order(self) -> list[list["CoreServiceType"]]:
-        for service in self.roots:
-            self._search(service)
-        return self.boot_paths
-
-
-class ServiceManager:
-    """
-    Manages services available for CORE nodes to use.
-    """
-
-    services: dict[str, type["CoreService"]] = {}
-
-    @classmethod
-    def add(cls, service: type["CoreService"]) -> None:
-        """
-        Add a service to manager.
-
-        :param service: service to add
-        :return: nothing
-        :raises ValueError: when service cannot be loaded
-        """
-        name = service.name
-        logger.debug("loading service: class(%s) name(%s)", service.__name__, name)
-        # avoid services with no name
-        if name is None:
-            logger.debug("not loading class(%s) with no name", service.__name__)
-            return
-        # avoid duplicate services
-        if name in cls.services:
-            raise ValueError(f"duplicate service being added: {name}")
-        # validate dependent executables are present
-        for executable in service.executables:
-            try:
-                utils.which(executable, required=True)
-            except CoreError as e:
-                raise CoreError(f"service({name}): {e}")
-        # validate service on load succeeds
-        try:
-            service.on_load()
-        except Exception as e:
-            logger.exception("error during service(%s) on load", service.name)
-            raise ValueError(e)
-        # make service available
-        cls.services[name] = service
-
-    @classmethod
-    def get(cls, name: str) -> type["CoreService"]:
-        """
-        Retrieve a service from the manager.
-
-        :param name: name of the service to retrieve
-        :return: service if it exists, None otherwise
-        """
-        service = cls.services.get(name)
-        if service is None:
-            raise CoreServiceError(f"service({name}) does not exist")
-        return service
-
-    @classmethod
-    def add_services(cls, path: Path) -> list[str]:
-        """
-        Method for retrieving all CoreServices from a given path.
-
-        :param path: path to retrieve services from
-        :return: list of core services that failed to load
-        """
-        service_errors = []
-        services = utils.load_classes(path, CoreService)
-        for service in services:
-            if not service.name:
-                continue
-            try:
-                cls.add(service)
-            except (CoreError, ValueError) as e:
-                service_errors.append(service.name)
-                logger.debug("not loading service(%s): %s", service.name, e)
-        return service_errors
-
-    @classmethod
-    def load_locals(cls) -> list[str]:
-        errors = []
-        for module_info in pkgutil.walk_packages(
-            core_services.__path__, f"{core_services.__name__}."
-        ):
-            services = utils.load_module(module_info.name, CoreService)
-            for service in services:
-                try:
-                    cls.add(service)
-                except CoreError as e:
-                    errors.append(service.name)
-                    logger.debug("not loading service(%s): %s", service.name, e)
-        return errors
-
-
-class CoreServices:
-    """
-    Class for interacting with a list of available startup services for
-    nodes. Mostly used to convert a CoreService into a Config API
-    message. This class lives in the Session object and remembers
-    the default services configured for each node type, and any
-    custom service configuration. A CoreService is not a Configurable.
-    """
-
-    name: str = "services"
-    config_type: RegisterTlvs = RegisterTlvs.UTILITY
-
-    def __init__(self, session: "Session") -> None:
-        """
-        Creates a CoreServices instance.
-
-        :param session: session this manager is tied to
-        """
-        self.session: "Session" = session
-        # dict of default services tuples, key is node type
-        self.default_services: dict[str, list[str]] = {
-            "mdr": ["zebra", "OSPFv3MDR", "IPForward"],
-            "PC": ["DefaultRoute"],
-            "prouter": [],
-            "router": ["zebra", "OSPFv2", "OSPFv3", "IPForward"],
-            "host": ["DefaultRoute", "SSH"],
-        }
-        # dict of node ids to dict of custom services by name
-        self.custom_services: dict[int, dict[str, "CoreService"]] = {}
-
-    def reset(self) -> None:
-        """
-        Called when config message with reset flag is received
-        """
-        self.custom_services.clear()
-
-    def get_service(
-        self, node_id: int, service_name: str, default_service: bool = False
-    ) -> "CoreService":
-        """
-        Get any custom service configured for the given node that matches the specified
-        service name. If no custom service is found, return the specified service.
-
-        :param node_id: object id to get service from
-        :param service_name: name of service to retrieve
-        :param default_service: True to return default service when custom does
-            not exist, False returns None
-        :return: custom service from the node
-        """
-        node_services = self.custom_services.setdefault(node_id, {})
-        default = None
-        if default_service:
-            default = ServiceManager.get(service_name)
-        return node_services.get(service_name, default)
-
-    def set_service(self, node_id: int, service_name: str) -> None:
-        """
-        Store service customizations in an instantiated service object
-        using a list of values that came from a config message.
-
-        :param node_id: object id to set custom service for
-        :param service_name: name of service to set
-        :return: nothing
-        """
-        logger.debug("setting custom service(%s) for node: %s", service_name, node_id)
-        service = self.get_service(node_id, service_name)
-        if not service:
-            service_class = ServiceManager.get(service_name)
-            service = service_class()
-
-            # add the custom service to dict
-            node_services = self.custom_services.setdefault(node_id, {})
-            node_services[service.name] = service
-
-    def add_services(
-        self, node: CoreNode, model: str, services: list[str] = None
-    ) -> None:
-        """
-        Add services to a node.
-
-        :param node: node to add services to
-        :param model: node model type to add services for
-        :param services: names of services to add to node
-        :return: nothing
-        """
-        if not services:
-            logger.info(
-                "using default services for node(%s) type(%s)", node.name, model
-            )
-            services = self.default_services.get(model, [])
-        logger.info("setting services for node(%s): %s", node.name, services)
-        for service_name in services:
-            service = self.get_service(node.id, service_name, default_service=True)
-            if not service:
-                logger.warning(
-                    "unknown service(%s) for node(%s)", service_name, node.name
-                )
-                continue
-            node.services.append(service)
-
-    def all_configs(self) -> list[tuple[int, "CoreService"]]:
-        """
-        Return (node_id, service) tuples for all stored configs. Used when reconnecting
-        to a session or opening XML.
-
-        :return: list of tuples of node ids and services
-        """
-        configs = []
-        for node_id in self.custom_services:
-            custom_services = self.custom_services[node_id]
-            for name in custom_services:
-                service = custom_services[name]
-                configs.append((node_id, service))
-        return configs
-
-    def all_files(self, service: "CoreService") -> list[tuple[str, str]]:
-        """
-        Return all customized files stored with a service.
-        Used when reconnecting to a session or opening XML.
-
-        :param service: service to get files for
-        :return: list of all custom service files
-        """
-        files = []
-        if not service.custom:
-            return files
-
-        for filename in service.configs:
-            data = service.config_data.get(filename)
-            if data is None:
-                continue
-            files.append((filename, data))
-
-        return files
-
-    def boot_services(self, node: CoreNode) -> None:
-        """
-        Start all services on a node.
-
-        :param node: node to start services on
-        :return: nothing
-        """
-        boot_paths = ServiceDependencies(node.services).boot_order()
-        funcs = []
-        for boot_path in boot_paths:
-            args = (node, boot_path)
-            funcs.append((self._boot_service_path, args, {}))
-        result, exceptions = utils.threadpool(funcs)
-        if exceptions:
-            raise CoreServiceBootError(*exceptions)
-
-    def _boot_service_path(self, node: CoreNode, boot_path: list["CoreServiceType"]):
-        logger.info(
-            "booting node(%s) services: %s",
-            node.name,
-            " -> ".join([x.name for x in boot_path]),
-        )
-        for service in boot_path:
-            service = self.get_service(node.id, service.name, default_service=True)
-            try:
-                self.boot_service(node, service)
-            except Exception as e:
-                logger.exception("exception booting service: %s", service.name)
-                raise CoreServiceBootError(e)
-
-    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.
-
-        :param node: node to boot services on
-        :param service: service to start
-        :return: nothing
-        """
-        logger.info(
-            "starting node(%s) service(%s) validation(%s)",
-            node.name,
-            service.name,
-            service.validation_mode.name,
-        )
-
-        # create service directories
-        for directory in service.dirs:
-            dir_path = Path(directory)
-            try:
-                node.create_dir(dir_path)
-            except (CoreCommandError, CoreError) as e:
-                logger.warning(
-                    "error mounting private dir '%s' for service '%s': %s",
-                    directory,
-                    service.name,
-                    e,
-                )
-
-        # create service files
-        self.create_service_files(node, service)
-
-        # run startup
-        wait = service.validation_mode == ServiceMode.BLOCKING
-        status = self.startup_service(node, service, wait)
-        if status:
-            raise CoreServiceBootError(
-                f"node({node.name}) service({service.name}) error during startup"
-            )
-
-        # blocking mode is finished
-        if wait:
-            return
-
-        # timer mode, sleep and return
-        if service.validation_mode == ServiceMode.TIMER:
-            time.sleep(service.validation_timer)
-        # non-blocking, attempt to validate periodically, up to validation_timer time
-        elif service.validation_mode == ServiceMode.NON_BLOCKING:
-            start = time.monotonic()
-            while True:
-                status = self.validate_service(node, service)
-                if not status:
-                    break
-
-                if time.monotonic() - start > service.validation_timer:
-                    break
-
-                time.sleep(service.validation_period)
-
-            if status:
-                raise CoreServiceBootError(
-                    f"node({node.name}) service({service.name}) failed validation"
-                )
-
-    def copy_service_file(self, node: CoreNode, file_path: Path, cfg: str) -> bool:
-        """
-        Given a configured service filename and config, determine if the
-        config references an existing file that should be copied.
-        Returns True for local files, False for generated.
-
-        :param node: node to copy service for
-        :param file_path: file name for a configured service
-        :param cfg: configuration string
-        :return: True if successful, False otherwise
-        """
-        if cfg[:7] == "file://":
-            src = cfg[7:]
-            src = src.split("\n")[0]
-            src = utils.expand_corepath(src, node.session, node)
-            # TODO: glob here
-            node.copy_file(src, file_path, mode=0o644)
-            return True
-        return False
-
-    def validate_service(self, node: CoreNode, service: "CoreServiceType") -> int:
-        """
-        Run the validation command(s) for a service.
-
-        :param node: node to validate service for
-        :param service: service to validate
-        :return: service validation status
-        """
-        logger.debug("validating node(%s) service(%s)", node.name, service.name)
-        cmds = service.validate
-        if not service.custom:
-            cmds = service.get_validate(node)
-
-        status = 0
-        for cmd in cmds:
-            logger.debug("validating service(%s) using: %s", service.name, cmd)
-            try:
-                node.cmd(cmd)
-            except CoreCommandError as e:
-                logger.debug(
-                    "node(%s) service(%s) validate failed", node.name, service.name
-                )
-                logger.debug("cmd(%s): %s", e.cmd, e.output)
-                status = -1
-                break
-
-        return status
-
-    def stop_services(self, node: CoreNode) -> None:
-        """
-        Stop all services on a node.
-
-        :param node: node to stop services on
-        :return: nothing
-        """
-        for service in node.services:
-            self.stop_service(node, service)
-
-    def stop_service(self, node: CoreNode, service: "CoreServiceType") -> int:
-        """
-        Stop a service on a node.
-
-        :param node: node to stop a service on
-        :param service: service to stop
-        :return: status for stopping the services
-        """
-        status = 0
-        for args in service.shutdown:
-            try:
-                node.cmd(args)
-            except CoreCommandError as e:
-                self.session.exception(
-                    ExceptionLevels.ERROR,
-                    "services",
-                    f"error stopping service {service.name}: {e.stderr}",
-                    node.id,
-                )
-                logger.exception("error running stop command %s", args)
-                status = -1
-        return status
-
-    def get_service_file(
-        self, node: CoreNode, service_name: str, filename: str
-    ) -> FileData:
-        """
-        Send a File Message when the GUI has requested a service file.
-        The file data is either auto-generated or comes from an existing config.
-
-        :param node: node to get service file from
-        :param service_name: service to get file from
-        :param filename: file name to retrieve
-        :return: file data
-        """
-        # get service to get file from
-        service = self.get_service(node.id, service_name, default_service=True)
-        if not service:
-            raise ValueError("invalid service: %s", service_name)
-
-        # retrieve config files for default/custom service
-        if service.custom:
-            config_files = service.configs
-        else:
-            config_files = service.get_configs(node)
-
-        if filename not in config_files:
-            raise ValueError(
-                "unknown service(%s) config file: %s", service_name, filename
-            )
-
-        # get the file data
-        data = service.config_data.get(filename)
-        if data is None:
-            data = service.generate_config(node, filename)
-        else:
-            data = data
-
-        filetypestr = f"service:{service.name}"
-        return FileData(
-            message_type=MessageFlags.ADD,
-            node=node.id,
-            name=filename,
-            type=filetypestr,
-            data=data,
-        )
-
-    def set_service_file(
-        self, node_id: int, service_name: str, file_name: str, data: str
-    ) -> None:
-        """
-        Receive a File Message from the GUI and store the customized file
-        in the service config. The filename must match one from the list of
-        config files in the service.
-
-        :param node_id: node id to set service file
-        :param service_name: service name to set file for
-        :param file_name: file name to set
-        :param data: data for file to set
-        :return: nothing
-        """
-        # attempt to set custom service, if needed
-        self.set_service(node_id, service_name)
-
-        # retrieve custom service
-        service = self.get_service(node_id, service_name)
-        if service is None:
-            logger.warning("received file name for unknown service: %s", service_name)
-            return
-
-        # validate file being set is valid
-        config_files = service.configs
-        if file_name not in config_files:
-            logger.warning(
-                "received unknown file(%s) for service(%s)", file_name, service_name
-            )
-            return
-
-        # set custom service file data
-        service.config_data[file_name] = data
-
-    def startup_service(
-        self, node: CoreNode, service: "CoreServiceType", wait: bool = False
-    ) -> int:
-        """
-        Startup a node service.
-
-        :param node: node to reconfigure service for
-        :param service: service to reconfigure
-        :param wait: determines if we should wait to validate startup
-        :return: status of startup
-        """
-        cmds = service.startup
-        if not service.custom:
-            cmds = service.get_startup(node)
-
-        status = 0
-        for cmd in cmds:
-            try:
-                node.cmd(cmd, wait)
-            except CoreCommandError:
-                logger.exception("error starting command")
-                status = -1
-        return status
-
-    def create_service_files(self, node: CoreNode, service: "CoreServiceType") -> None:
-        """
-        Creates node service files.
-
-        :param node: node to reconfigure service for
-        :param service: service to reconfigure
-        :return: nothing
-        """
-        # get values depending on if custom or not
-        config_files = service.configs
-        if not service.custom:
-            config_files = service.get_configs(node)
-        for file_name in config_files:
-            file_path = Path(file_name)
-            logger.debug(
-                "generating service config custom(%s): %s", service.custom, file_name
-            )
-            if service.custom:
-                cfg = service.config_data.get(file_name)
-                if cfg is None:
-                    cfg = service.generate_config(node, file_name)
-                # cfg may have a file:/// url for copying from a file
-                try:
-                    if self.copy_service_file(node, file_path, cfg):
-                        continue
-                except OSError:
-                    logger.exception("error copying service file: %s", file_name)
-                    continue
-            else:
-                cfg = service.generate_config(node, file_name)
-            node.create_file(file_path, cfg)
-
-    def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None:
-        """
-        Reconfigure a node service.
-
-        :param node: node to reconfigure service for
-        :param service: service to reconfigure
-        :return: nothing
-        """
-        config_files = service.configs
-        if not service.custom:
-            config_files = service.get_configs(node)
-        for file_name in config_files:
-            file_path = Path(file_name)
-            if file_name[:7] == "file:///":
-                # TODO: implement this
-                raise NotImplementedError
-            cfg = service.config_data.get(file_name)
-            if cfg is None:
-                cfg = service.generate_config(node, file_name)
-            node.create_file(file_path, cfg)
-
-
-class CoreService:
-    """
-    Parent class used for defining services.
-    """
-
-    # service name should not include spaces
-    name: Optional[str] = None
-
-    # executables that must exist for service to run
-    executables: tuple[str, ...] = ()
-
-    # sets service requirements that must be started prior to this service starting
-    dependencies: tuple[str, ...] = ()
-
-    # group string allows grouping services together
-    group: Optional[str] = None
-
-    # private, per-node directories required by this service
-    dirs: tuple[str, ...] = ()
-
-    # config files written by this service
-    configs: tuple[str, ...] = ()
-
-    # config file data
-    config_data: dict[str, str] = {}
-
-    # list of startup commands
-    startup: tuple[str, ...] = ()
-
-    # list of shutdown commands
-    shutdown: tuple[str, ...] = ()
-
-    # list of validate commands
-    validate: tuple[str, ...] = ()
-
-    # validation mode, used to determine startup success
-    validation_mode: ServiceMode = ServiceMode.NON_BLOCKING
-
-    # time to wait in seconds for determining if service started successfully
-    validation_timer: int = 5
-
-    # validation period in seconds, how frequent validation is attempted
-    validation_period: float = 0.5
-
-    # metadata associated with this service
-    meta: Optional[str] = None
-
-    # custom configuration text
-    custom: bool = False
-    custom_needed: bool = False
-
-    def __init__(self) -> None:
-        """
-        Services are not necessarily instantiated. Classmethods may be used
-        against their config. Services are instantiated when a custom
-        configuration is used to override their default parameters.
-        """
-        self.custom: bool = True
-        self.config_data: dict[str, str] = self.__class__.config_data.copy()
-
-    @classmethod
-    def on_load(cls) -> None:
-        pass
-
-    @classmethod
-    def get_configs(cls, node: CoreNode) -> Iterable[str]:
-        """
-        Return the tuple of configuration file filenames. This default method
-        returns the cls._configs tuple, but this method may be overriden to
-        provide node-specific filenames that may be based on other services.
-
-        :param node: node to generate config for
-        :return: configuration files
-        """
-        return cls.configs
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate configuration file given a node object. The filename is
-        provided to allow for multiple config files.
-        Return the configuration string to be written to a file or sent
-        to the GUI for customization.
-
-        :param node: node to generate config for
-        :param filename: file name to generate config for
-        :return: generated config
-        """
-        raise NotImplementedError
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> Iterable[str]:
-        """
-        Return the tuple of startup commands. This default method
-        returns the cls.startup tuple, but this method may be
-        overridden to provide node-specific commands that may be
-        based on other services.
-
-        :param node: node to get startup for
-        :return: startup commands
-        """
-        return cls.startup
-
-    @classmethod
-    def get_validate(cls, node: CoreNode) -> Iterable[str]:
-        """
-        Return the tuple of validate commands. This default method
-        returns the cls.validate tuple, but this method may be
-        overridden to provide node-specific commands that may be
-        based on other services.
-
-        :param node: node to validate
-        :return: validation commands
-        """
-        return cls.validate
diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py
deleted file mode 100644
index 43cd9af41..000000000
--- a/daemon/core/services/emaneservices.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from core.emane.nodes import EmaneNet
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService
-from core.xml import emanexml
-
-
-class EmaneTransportService(CoreService):
-    name: str = "transportd"
-    group: str = "EMANE"
-    executables: tuple[str, ...] = ("emanetransportd", "emanegentransportxml")
-    dependencies: tuple[str, ...] = ()
-    dirs: tuple[str, ...] = ()
-    configs: tuple[str, ...] = ("emanetransport.sh",)
-    startup: tuple[str, ...] = (f"bash {configs[0]}",)
-    validate: tuple[str, ...] = (f"pidof {executables[0]}",)
-    validation_timer: float = 0.5
-    shutdown: tuple[str, ...] = (f"killall {executables[0]}",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        emane_manager = node.session.emane
-        cfg = ""
-        for iface in node.get_ifaces():
-            if not isinstance(iface.net, EmaneNet):
-                continue
-            emane_net = iface.net
-            config = emane_manager.get_iface_config(emane_net, iface)
-            if emanexml.is_external(config):
-                nem_id = emane_manager.get_nem_id(iface)
-                cfg += f"emanegentransportxml {iface.name}-platform.xml\n"
-                cfg += f"emanetransportd -r -l 0 -d transportdaemon{nem_id}.xml\n"
-        return cfg
diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py
deleted file mode 100644
index 28756c19e..000000000
--- a/daemon/core/services/frr.py
+++ /dev/null
@@ -1,683 +0,0 @@
-"""
-frr.py: defines routing services provided by FRRouting.
-Assumes installation of FRR via https://deb.frrouting.org/
-"""
-from typing import Optional
-
-import netaddr
-
-from core.emane.nodes import EmaneNet
-from core.nodes.base import CoreNode, NodeBase
-from core.nodes.interface import DEFAULT_MTU, CoreInterface
-from core.nodes.network import PtpNet, WlanNode
-from core.nodes.physical import Rj45Node
-from core.nodes.wireless import WirelessNode
-from core.services.coreservices import CoreService
-
-FRR_STATE_DIR: str = "/var/run/frr"
-
-
-def is_wireless(node: NodeBase) -> bool:
-    """
-    Check if the node is a wireless type node.
-
-    :param node: node to check type for
-    :return: True if wireless type, False otherwise
-    """
-    return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
-
-
-class FRRZebra(CoreService):
-    name: str = "FRRzebra"
-    group: str = "FRR"
-    dirs: tuple[str, ...] = ("/usr/local/etc/frr", "/var/run/frr", "/var/log/frr")
-    configs: tuple[str, ...] = (
-        "/usr/local/etc/frr/frr.conf",
-        "frrboot.sh",
-        "/usr/local/etc/frr/vtysh.conf",
-        "/usr/local/etc/frr/daemons",
-    )
-    startup: tuple[str, ...] = ("bash frrboot.sh zebra",)
-    shutdown: tuple[str, ...] = ("killall zebra",)
-    validate: tuple[str, ...] = ("pidof zebra",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the frr.conf or frrboot.sh file contents.
-        """
-        if filename == cls.configs[0]:
-            return cls.generate_frr_conf(node)
-        elif filename == cls.configs[1]:
-            return cls.generate_frr_boot(node)
-        elif filename == cls.configs[2]:
-            return cls.generate_vtysh_conf(node)
-        elif filename == cls.configs[3]:
-            return cls.generate_frr_daemons(node)
-        else:
-            raise ValueError(
-                "file name (%s) is not a known configuration: %s", filename, cls.configs
-            )
-
-    @classmethod
-    def generate_vtysh_conf(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text.
-        """
-        return "service integrated-vtysh-config\n"
-
-    @classmethod
-    def generate_frr_conf(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text. Other services that depend on zebra
-        will have hooks that are invoked here.
-        """
-        # we could verify here that filename == frr.conf
-        cfg = ""
-        for iface in node.get_ifaces():
-            cfg += f"interface {iface.name}\n"
-            # include control interfaces in addressing but not routing daemons
-            if iface.control:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ips()))
-                cfg += "\n"
-                continue
-            cfgv4 = ""
-            cfgv6 = ""
-            want_ipv4 = False
-            want_ipv6 = False
-            for s in node.services:
-                if cls.name not in s.dependencies:
-                    continue
-                if not (isinstance(s, FrrService) or issubclass(s, FrrService)):
-                    continue
-                iface_config = s.generate_frr_iface_config(node, iface)
-                if s.ipv4_routing:
-                    want_ipv4 = True
-                if s.ipv6_routing:
-                    want_ipv6 = True
-                    cfgv6 += iface_config
-                else:
-                    cfgv4 += iface_config
-
-            if want_ipv4:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ip4s))
-                cfg += "\n"
-                cfg += cfgv4
-            if want_ipv6:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ip6s))
-                cfg += "\n"
-                cfg += cfgv6
-            cfg += "!\n"
-
-        for s in node.services:
-            if cls.name not in s.dependencies:
-                continue
-            if not (isinstance(s, FrrService) or issubclass(s, FrrService)):
-                continue
-            cfg += s.generate_frr_config(node)
-        return cfg
-
-    @staticmethod
-    def addrstr(ip: netaddr.IPNetwork) -> str:
-        """
-        helper for mapping IP addresses to zebra config statements
-        """
-        address = str(ip.ip)
-        if netaddr.valid_ipv4(address):
-            return f"ip address {ip}"
-        elif netaddr.valid_ipv6(address):
-            return f"ipv6 address {ip}"
-        else:
-            raise ValueError(f"invalid address: {ip}")
-
-    @classmethod
-    def generate_frr_boot(cls, node: CoreNode) -> str:
-        """
-        Generate a shell script used to boot the FRR daemons.
-        """
-        frr_bin_search = node.session.options.get(
-            "frr_bin_search", '"/usr/local/bin /usr/bin /usr/lib/frr"'
-        )
-        frr_sbin_search = node.session.options.get(
-            "frr_sbin_search",
-            '"/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr"',
-        )
-        cfg = f"""\
-#!/bin/sh
-# auto-generated by zebra service (frr.py)
-FRR_CONF={cls.configs[0]}
-FRR_SBIN_SEARCH={frr_sbin_search}
-FRR_BIN_SEARCH={frr_bin_search}
-FRR_STATE_DIR={FRR_STATE_DIR}
-
-searchforprog()
-{{
-    prog=$1
-    searchpath=$@
-    ret=
-    for p in $searchpath; do
-        if [ -x $p/$prog ]; then
-            ret=$p
-            break
-        fi
-    done
-    echo $ret
-}}
-
-confcheck()
-{{
-    CONF_DIR=`dirname $FRR_CONF`
-    # if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR
-    if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then
-        ln -s $CONF_DIR/frr.conf /etc/frr/frr.conf
-    fi
-    # if /etc/frr exists, point /etc/frr/vtysh.conf -> CONF_DIR
-    if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then
-        ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf
-    fi
-}}
-
-bootdaemon()
-{{
-    FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH)
-    if [ "z$FRR_SBIN_DIR" = "z" ]; then
-        echo "ERROR: FRR's '$1' daemon not found in search path:"
-        echo "  $FRR_SBIN_SEARCH"
-        return 1
-    fi
-
-    flags=""
-
-    if [ "$1" = "pimd" ] && \\
-        grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $FRR_CONF; then
-        flags="$flags -6"
-    fi
-
-    if [ "$1" = "ospfd" ]; then
-        flags="$flags --apiserver"
-    fi
-
-    #force FRR to use CORE generated conf file
-    flags="$flags -d -f $FRR_CONF"
-    $FRR_SBIN_DIR/$1 $flags
-
-    if [ "$?" != "0" ]; then
-        echo "ERROR: FRR's '$1' daemon failed to start!:"
-        return 1
-    fi
-}}
-
-bootfrr()
-{{
-    FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH)
-    if [ "z$FRR_BIN_DIR" = "z" ]; then
-        echo "ERROR: FRR's 'vtysh' program not found in search path:"
-        echo "  $FRR_BIN_SEARCH"
-        return 1
-    fi
-
-    # fix /var/run/frr permissions
-    id -u frr 2>/dev/null >/dev/null
-    if [ "$?" = "0" ]; then
-        chown frr $FRR_STATE_DIR
-    fi
-
-    bootdaemon "zebra"
-    if grep -q "^ip route " $FRR_CONF; then
-        bootdaemon "staticd"
-    fi
-    for r in rip ripng ospf6 ospf bgp babel isis; do
-        if grep -q "^router \\<${{r}}\\>" $FRR_CONF; then
-            bootdaemon "${{r}}d"
-        fi
-    done
-
-    if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $FRR_CONF; then
-        bootdaemon "pimd"
-    fi
-
-    $FRR_BIN_DIR/vtysh -b
-}}
-
-if [ "$1" != "zebra" ]; then
-    echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!"
-    exit 1
-fi
-
-confcheck
-bootfrr
-"""
-        for iface in node.get_ifaces():
-            cfg += f"ip link set dev {iface.name} down\n"
-            cfg += "sleep 1\n"
-            cfg += f"ip link set dev {iface.name} up\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_daemons(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text.
-        """
-        return """\
-#
-# When activation a daemon at the first time, a config file, even if it is
-# empty, has to be present *and* be owned by the user and group "frr", else
-# the daemon will not be started by /etc/init.d/frr. The permissions should
-# be u=rw,g=r,o=.
-# When using "vtysh" such a config file is also needed. It should be owned by
-# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
-#
-# The watchfrr and zebra daemons are always started.
-#
-bgpd=yes
-ospfd=yes
-ospf6d=yes
-ripd=yes
-ripngd=yes
-isisd=yes
-pimd=yes
-ldpd=yes
-nhrpd=yes
-eigrpd=yes
-babeld=yes
-sharpd=yes
-staticd=yes
-pbrd=yes
-bfdd=yes
-fabricd=yes
-
-#
-# If this option is set the /etc/init.d/frr script automatically loads
-# the config via "vtysh -b" when the servers are started.
-# Check /etc/pam.d/frr if you intend to use "vtysh"!
-#
-vtysh_enable=yes
-zebra_options="  -A 127.0.0.1 -s 90000000"
-bgpd_options="   -A 127.0.0.1"
-ospfd_options="  -A 127.0.0.1"
-ospf6d_options=" -A ::1"
-ripd_options="   -A 127.0.0.1"
-ripngd_options=" -A ::1"
-isisd_options="  -A 127.0.0.1"
-pimd_options="   -A 127.0.0.1"
-ldpd_options="   -A 127.0.0.1"
-nhrpd_options="  -A 127.0.0.1"
-eigrpd_options=" -A 127.0.0.1"
-babeld_options=" -A 127.0.0.1"
-sharpd_options=" -A 127.0.0.1"
-pbrd_options="   -A 127.0.0.1"
-staticd_options="-A 127.0.0.1"
-bfdd_options="   -A 127.0.0.1"
-fabricd_options="-A 127.0.0.1"
-
-# The list of daemons to watch is automatically generated by the init script.
-#watchfrr_options=""
-
-# for debugging purposes, you can specify a "wrap" command to start instead
-# of starting the daemon directly, e.g. to use valgrind on ospfd:
-#   ospfd_wrap="/usr/bin/valgrind"
-# or you can use "all_wrap" for all daemons, e.g. to use perf record:
-#   all_wrap="/usr/bin/perf record --call-graph -"
-# the normal daemon command is added to this at the end.
-"""
-
-
-class FrrService(CoreService):
-    """
-    Parent class for FRR services. Defines properties and methods
-    common to FRR's routing daemons.
-    """
-
-    name: Optional[str] = None
-    group: str = "FRR"
-    dependencies: tuple[str, ...] = ("FRRzebra",)
-    meta: str = "The config file for this service can be found in the Zebra service."
-    ipv4_routing: bool = False
-    ipv6_routing: bool = False
-
-    @staticmethod
-    def router_id(node: CoreNode) -> str:
-        """
-        Helper to return the first IPv4 address of a node as its router ID.
-        """
-        for iface in node.get_ifaces(control=False):
-            ip4 = iface.get_ip4()
-            if ip4:
-                return str(ip4.ip)
-        return "0.0.0.0"
-
-    @staticmethod
-    def rj45check(iface: CoreInterface) -> bool:
-        """
-        Helper to detect whether interface is connected an external RJ45
-        link.
-        """
-        if iface.net:
-            for peer_iface in iface.net.get_ifaces():
-                if peer_iface == iface:
-                    continue
-                if isinstance(peer_iface.node, Rj45Node):
-                    return True
-        return False
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return ""
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        return ""
-
-
-class FRROspfv2(FrrService):
-    """
-    The OSPFv2 service provides IPv4 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified frr.conf file.
-    """
-
-    name: str = "FRROSPFv2"
-    shutdown: tuple[str, ...] = ("killall ospfd",)
-    validate: tuple[str, ...] = ("pidof ospfd",)
-    ipv4_routing: bool = True
-
-    @staticmethod
-    def mtu_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect MTU mismatch and add the appropriate OSPF
-        mtu-ignore command. This is needed when e.g. a node is linked via a
-        GreTap device.
-        """
-        if iface.mtu != DEFAULT_MTU:
-            # a workaround for PhysicalNode GreTap, which has no knowledge of
-            # the other nodes/nets
-            return "  ip ospf mtu-ignore\n"
-        if not iface.net:
-            return ""
-        for iface in iface.net.get_ifaces():
-            if iface.mtu != iface.mtu:
-                return "  ip ospf mtu-ignore\n"
-        return ""
-
-    @staticmethod
-    def ptp_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect whether interface is connected to a notional
-        point-to-point link.
-        """
-        if isinstance(iface.net, PtpNet):
-            return "  ip ospf network point-to-point\n"
-        return ""
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = "router ospf\n"
-        rtrid = cls.router_id(node)
-        cfg += f"  router-id {rtrid}\n"
-        # network 10.0.0.0/24 area 0
-        for iface in node.get_ifaces(control=False):
-            for ip4 in iface.ip4s:
-                cfg += f"  network {ip4} area 0\n"
-        cfg += "  ospf opaque-lsa\n"
-        cfg += "!\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        cfg = cls.mtu_check(iface)
-        # external RJ45 connections will use default OSPF timers
-        if cls.rj45check(iface):
-            return cfg
-        cfg += cls.ptp_check(iface)
-        return (
-            cfg
-            + """\
-  ip ospf hello-interval 2
-  ip ospf dead-interval 6
-  ip ospf retransmit-interval 5
-"""
-        )
-
-
-class FRROspfv3(FrrService):
-    """
-    The OSPFv3 service provides IPv6 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified frr.conf file.
-    """
-
-    name: str = "FRROSPFv3"
-    shutdown: tuple[str, ...] = ("killall ospf6d",)
-    validate: tuple[str, ...] = ("pidof ospf6d",)
-    ipv4_routing: bool = True
-    ipv6_routing: bool = True
-
-    @staticmethod
-    def min_mtu(iface: CoreInterface) -> int:
-        """
-        Helper to discover the minimum MTU of interfaces linked with the
-        given interface.
-        """
-        mtu = iface.mtu
-        if not iface.net:
-            return mtu
-        for iface in iface.net.get_ifaces():
-            if iface.mtu < mtu:
-                mtu = iface.mtu
-        return mtu
-
-    @classmethod
-    def mtu_check(cls, iface: CoreInterface) -> str:
-        """
-        Helper to detect MTU mismatch and add the appropriate OSPFv3
-        ifmtu command. This is needed when e.g. a node is linked via a
-        GreTap device.
-        """
-        minmtu = cls.min_mtu(iface)
-        if minmtu < iface.mtu:
-            return f"  ipv6 ospf6 ifmtu {minmtu:d}\n"
-        else:
-            return ""
-
-    @staticmethod
-    def ptp_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect whether interface is connected to a notional
-        point-to-point link.
-        """
-        if isinstance(iface.net, PtpNet):
-            return "  ipv6 ospf6 network point-to-point\n"
-        return ""
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = "router ospf6\n"
-        rtrid = cls.router_id(node)
-        cfg += f"  router-id {rtrid}\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"  interface {iface.name} area 0.0.0.0\n"
-        cfg += "!\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return cls.mtu_check(iface)
-
-
-class FRRBgp(FrrService):
-    """
-    The BGP service provides interdomain routing.
-    Peers must be manually configured, with a full mesh for those
-    having the same AS number.
-    """
-
-    name: str = "FRRBGP"
-    shutdown: tuple[str, ...] = ("killall bgpd",)
-    validate: tuple[str, ...] = ("pidof bgpd",)
-    custom_needed: bool = True
-    ipv4_routing: bool = True
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = "!\n! BGP configuration\n!\n"
-        cfg += "! You should configure the AS number below,\n"
-        cfg += "! along with this router's peers.\n!\n"
-        cfg += f"router bgp {node.id}\n"
-        rtrid = cls.router_id(node)
-        cfg += f"  bgp router-id {rtrid}\n"
-        cfg += "  redistribute connected\n"
-        cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n"
-        return cfg
-
-
-class FRRRip(FrrService):
-    """
-    The RIP service provides IPv4 routing for wired networks.
-    """
-
-    name: str = "FRRRIP"
-    shutdown: tuple[str, ...] = ("killall ripd",)
-    validate: tuple[str, ...] = ("pidof ripd",)
-    ipv4_routing: bool = True
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = """\
-router rip
-  redistribute static
-  redistribute connected
-  redistribute ospf
-  network 0.0.0.0/0
-!
-"""
-        return cfg
-
-
-class FRRRipng(FrrService):
-    """
-    The RIP NG service provides IPv6 routing for wired networks.
-    """
-
-    name: str = "FRRRIPNG"
-    shutdown: tuple[str, ...] = ("killall ripngd",)
-    validate: tuple[str, ...] = ("pidof ripngd",)
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = """\
-router ripng
-  redistribute static
-  redistribute connected
-  redistribute ospf6
-  network ::/0
-!
-"""
-        return cfg
-
-
-class FRRBabel(FrrService):
-    """
-    The Babel service provides a loop-avoiding distance-vector routing
-    protocol for IPv6 and IPv4 with fast convergence properties.
-    """
-
-    name: str = "FRRBabel"
-    shutdown: tuple[str, ...] = ("killall babeld",)
-    validate: tuple[str, ...] = ("pidof babeld",)
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = "router babel\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"  network {iface.name}\n"
-        cfg += "  redistribute static\n  redistribute ipv4 connected\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        if is_wireless(iface.net):
-            return "  babel wireless\n  no babel split-horizon\n"
-        else:
-            return "  babel wired\n  babel split-horizon\n"
-
-
-class FRRpimd(FrrService):
-    """
-    PIM multicast routing based on XORP.
-    """
-
-    name: str = "FRRpimd"
-    shutdown: tuple[str, ...] = ("killall pimd",)
-    validate: tuple[str, ...] = ("pidof pimd",)
-    ipv4_routing: bool = True
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        ifname = "eth0"
-        for iface in node.get_ifaces():
-            if iface.name != "lo":
-                ifname = iface.name
-                break
-        cfg = "router mfea\n!\n"
-        cfg += "router igmp\n!\n"
-        cfg += "router pim\n"
-        cfg += "  !ip pim rp-address 10.0.0.1\n"
-        cfg += f"  ip pim bsr-candidate {ifname}\n"
-        cfg += f"  ip pim rp-candidate {ifname}\n"
-        cfg += "  !ip pim spt-threshold interval 10 bytes 80000\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return "  ip mfea\n  ip igmp\n  ip pim\n"
-
-
-class FRRIsis(FrrService):
-    """
-    The ISIS service provides IPv4 and IPv6 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified frr.conf file.
-    """
-
-    name: str = "FRRISIS"
-    shutdown: tuple[str, ...] = ("killall isisd",)
-    validate: tuple[str, ...] = ("pidof isisd",)
-    ipv4_routing: bool = True
-    ipv6_routing: bool = True
-
-    @staticmethod
-    def ptp_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect whether interface is connected to a notional
-        point-to-point link.
-        """
-        if isinstance(iface.net, PtpNet):
-            return "  isis network point-to-point\n"
-        return ""
-
-    @classmethod
-    def generate_frr_config(cls, node: CoreNode) -> str:
-        cfg = "router isis DEFAULT\n"
-        cfg += f"  net 47.0001.0000.1900.{node.id:04x}.00\n"
-        cfg += "  metric-style wide\n"
-        cfg += "  is-type level-2-only\n"
-        cfg += "!\n"
-        return cfg
-
-    @classmethod
-    def generate_frr_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        cfg = "  ip router isis DEFAULT\n"
-        cfg += "  ipv6 router isis DEFAULT\n"
-        cfg += "  isis circuit-type level-2-only\n"
-        cfg += cls.ptp_check(iface)
-        return cfg
diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py
deleted file mode 100644
index 32e19f606..000000000
--- a/daemon/core/services/nrl.py
+++ /dev/null
@@ -1,582 +0,0 @@
-"""
-nrl.py: defines services provided by NRL protolib tools hosted here:
-    http://www.nrl.navy.mil/itd/ncs/products
-"""
-from typing import Optional
-
-from core import utils
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService
-
-
-class NrlService(CoreService):
-    """
-    Parent class for NRL services. Defines properties and methods
-    common to NRL's routing daemons.
-    """
-
-    name: Optional[str] = None
-    group: str = "ProtoSvc"
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-    @staticmethod
-    def firstipv4prefix(node: CoreNode, prefixlen: int = 24) -> str:
-        """
-        Similar to QuaggaService.routerid(). Helper to return the first IPv4
-        prefix of a node, using the supplied prefix length. This ignores the
-        interface's prefix length, so e.g. '/32' can turn into '/24'.
-        """
-        for iface in node.get_ifaces(control=False):
-            ip4 = iface.get_ip4()
-            if ip4:
-                return f"{ip4.ip}/{prefixlen}"
-        return f"0.0.0.0/{prefixlen}"
-
-
-class MgenSinkService(NrlService):
-    name: str = "MGEN_Sink"
-    executables: tuple[str, ...] = ("mgen",)
-    configs: tuple[str, ...] = ("sink.mgen",)
-    startup: tuple[str, ...] = ("mgen input sink.mgen",)
-    validate: tuple[str, ...] = ("pidof mgen",)
-    shutdown: tuple[str, ...] = ("killall mgen",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        cfg = "0.0 LISTEN UDP 5000\n"
-        for iface in node.get_ifaces():
-            name = utils.sysctl_devname(iface.name)
-            cfg += f"0.0 Join 224.225.1.2 INTERFACE {name}\n"
-        return cfg
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> tuple[str, ...]:
-        cmd = cls.startup[0]
-        cmd += f" output /tmp/mgen_{node.name}.log"
-        return (cmd,)
-
-
-class NrlNhdp(NrlService):
-    """
-    NeighborHood Discovery Protocol for MANET networks.
-    """
-
-    name: str = "NHDP"
-    executables: tuple[str, ...] = ("nrlnhdp",)
-    startup: tuple[str, ...] = ("nrlnhdp",)
-    shutdown: tuple[str, ...] = ("killall nrlnhdp",)
-    validate: tuple[str, ...] = ("pidof nrlnhdp",)
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> tuple[str, ...]:
-        """
-        Generate the appropriate command-line based on node interfaces.
-        """
-        cmd = cls.startup[0]
-        cmd += " -l /var/log/nrlnhdp.log"
-        cmd += f" -rpipe {node.name}_nhdp"
-        servicenames = map(lambda x: x.name, node.services)
-        if "SMF" in servicenames:
-            cmd += " -flooding ecds"
-            cmd += f" -smfClient {node.name}_smf"
-        ifaces = node.get_ifaces(control=False)
-        if len(ifaces) > 0:
-            iface_names = map(lambda x: x.name, ifaces)
-            cmd += " -i "
-            cmd += " -i ".join(iface_names)
-        return (cmd,)
-
-
-class NrlSmf(NrlService):
-    """
-    Simplified Multicast Forwarding for MANET networks.
-    """
-
-    name: str = "SMF"
-    executables: tuple[str, ...] = ("nrlsmf",)
-    startup: tuple[str, ...] = ("bash startsmf.sh",)
-    shutdown: tuple[str, ...] = ("killall nrlsmf",)
-    validate: tuple[str, ...] = ("pidof nrlsmf",)
-    configs: tuple[str, ...] = ("startsmf.sh",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a startup script for SMF. Because nrlsmf does not
-        daemonize, it can cause problems in some situations when launched
-        directly using vcmd.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by nrl.py:NrlSmf.generateconfig()\n"
-        comments = ""
-        cmd = f"nrlsmf instance {node.name}_smf"
-
-        servicenames = map(lambda x: x.name, node.services)
-        ifaces = node.get_ifaces(control=False)
-        if len(ifaces) == 0:
-            return ""
-        if len(ifaces) > 0:
-            if "NHDP" in servicenames:
-                comments += "# NHDP service is enabled\n"
-                cmd += " ecds "
-            elif "OLSR" in servicenames:
-                comments += "# OLSR service is enabled\n"
-                cmd += " smpr "
-            else:
-                cmd += " cf "
-            iface_names = map(lambda x: x.name, ifaces)
-            cmd += ",".join(iface_names)
-
-        cmd += " hash MD5"
-        cmd += " log /var/log/nrlsmf.log"
-        cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n"
-        return cfg
-
-
-class NrlOlsr(NrlService):
-    """
-    Optimized Link State Routing protocol for MANET networks.
-    """
-
-    name: str = "OLSR"
-    executables: tuple[str, ...] = ("nrlolsrd",)
-    startup: tuple[str, ...] = ("nrlolsrd",)
-    shutdown: tuple[str, ...] = ("killall nrlolsrd",)
-    validate: tuple[str, ...] = ("pidof nrlolsrd",)
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> tuple[str, ...]:
-        """
-        Generate the appropriate command-line based on node interfaces.
-        """
-        cmd = cls.startup[0]
-        # are multiple interfaces supported? No.
-        ifaces = node.get_ifaces()
-        if len(ifaces) > 0:
-            iface = ifaces[0]
-            cmd += f" -i {iface.name}"
-        cmd += " -l /var/log/nrlolsrd.log"
-        cmd += f" -rpipe {node.name}_olsr"
-        servicenames = map(lambda x: x.name, node.services)
-        if "SMF" in servicenames and "NHDP" not in servicenames:
-            cmd += " -flooding s-mpr"
-            cmd += f" -smfClient {node.name}_smf"
-        if "zebra" in servicenames:
-            cmd += " -z"
-        return (cmd,)
-
-
-class NrlOlsrv2(NrlService):
-    """
-    Optimized Link State Routing protocol version 2 for MANET networks.
-    """
-
-    name: str = "OLSRv2"
-    executables: tuple[str, ...] = ("nrlolsrv2",)
-    startup: tuple[str, ...] = ("nrlolsrv2",)
-    shutdown: tuple[str, ...] = ("killall nrlolsrv2",)
-    validate: tuple[str, ...] = ("pidof nrlolsrv2",)
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> tuple[str, ...]:
-        """
-        Generate the appropriate command-line based on node interfaces.
-        """
-        cmd = cls.startup[0]
-        cmd += " -l /var/log/nrlolsrv2.log"
-        cmd += f" -rpipe {node.name}_olsrv2"
-        servicenames = map(lambda x: x.name, node.services)
-        if "SMF" in servicenames:
-            cmd += " -flooding ecds"
-            cmd += f" -smfClient {node.name}_smf"
-        cmd += " -p olsr"
-        ifaces = node.get_ifaces(control=False)
-        if len(ifaces) > 0:
-            iface_names = map(lambda x: x.name, ifaces)
-            cmd += " -i "
-            cmd += " -i ".join(iface_names)
-        return (cmd,)
-
-
-class OlsrOrg(NrlService):
-    """
-    Optimized Link State Routing protocol from olsr.org for MANET networks.
-    """
-
-    name: str = "OLSRORG"
-    executables: tuple[str, ...] = ("olsrd",)
-    configs: tuple[str, ...] = ("/etc/olsrd/olsrd.conf",)
-    dirs: tuple[str, ...] = ("/etc/olsrd",)
-    startup: tuple[str, ...] = ("olsrd",)
-    shutdown: tuple[str, ...] = ("killall olsrd",)
-    validate: tuple[str, ...] = ("pidof olsrd",)
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> tuple[str, ...]:
-        """
-        Generate the appropriate command-line based on node interfaces.
-        """
-        cmd = cls.startup[0]
-        ifaces = node.get_ifaces(control=False)
-        if len(ifaces) > 0:
-            iface_names = map(lambda x: x.name, ifaces)
-            cmd += " -i "
-            cmd += " -i ".join(iface_names)
-        return (cmd,)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a default olsrd config file to use the broadcast address of
-        255.255.255.255.
-        """
-        cfg = """\
-#
-# OLSR.org routing daemon config file
-# This file contains the usual options for an ETX based
-# stationary network without fisheye
-# (for other options see olsrd.conf.default.full)
-#
-# Lines starting with a # are discarded
-#
-
-#### ATTENTION for IPv6 users ####
-# Because of limitations in the parser IPv6 addresses must NOT
-# begin with a ":", so please add a "0" as a prefix.
-
-###########################
-### Basic configuration ###
-###########################
-# keep this settings at the beginning of your first configuration file
-
-# Debug level (0-9)
-# If set to 0 the daemon runs in the background, unless "NoFork" is set to true
-# (Default is 1)
-
-# DebugLevel  1
-
-# IP version to use (4 or 6)
-# (Default is 4)
-
-# IpVersion 4
-
-#################################
-### OLSRd agent configuration ###
-#################################
-# this parameters control the settings of the routing agent which are not
-# related to the OLSR protocol and it's extensions
-
-# FIBMetric controls the metric value of the host-routes OLSRd sets.
-# - "flat" means that the metric value is always 2. This is the preferred value
-#   because it helps the linux kernel routing to clean up older routes
-# - "correct" use the hopcount as the metric value.
-# - "approx" use the hopcount as the metric value too, but does only update the
-#   hopcount if the nexthop changes too
-# (Default is "flat")
-
-# FIBMetric "flat"
-
-#######################################
-### Linux specific OLSRd extensions ###
-#######################################
-# these parameters are only working on linux at the moment
-
-# SrcIpRoutes tells OLSRd to set the Src flag of host routes to the originator-ip
-# of the node. In addition to this an additional localhost device is created
-# to make sure the returning traffic can be received.
-# (Default is "no")
-
-# SrcIpRoutes no
-
-# Specify the proto tag to be used for routes olsr inserts into kernel
-# currently only implemented for linux
-# valid values under linux are 1 .. 254
-# 1 gets remapped by olsrd to 0 UNSPECIFIED (1 is reserved for ICMP redirects)
-# 2 KERNEL routes (not very wise to use)
-# 3 BOOT (should in fact not be used by routing daemons)
-# 4 STATIC
-# 8 .. 15 various routing daemons (gated, zebra, bird, & co)
-# (defaults to 0 which gets replaced by an OS-specific default value
-# under linux 3 (BOOT) (for backward compatibility)
-
-# RtProto 0
-
-# Activates (in IPv6 mode) the automatic use of NIIT
-# (see README-Olsr-Extensions)
-# (default is "yes")
-
-# UseNiit yes
-
-# Activates the smartgateway ipip tunnel feature.
-# See README-Olsr-Extensions for a description of smartgateways.
-# (default is "no")
-
-# SmartGateway no
-
-# Signals that the server tunnel must always be removed on shutdown,
-# irrespective of the interface up/down state during startup.
-# (default is "no")
-
-# SmartGatewayAlwaysRemoveServerTunnel no
-
-# Determines the maximum number of gateways that can be in use at any given
-# time. This setting is used to mitigate the effects of breaking connections
-# (due to the selection of a new gateway) on a dynamic network.
-# (default is 1)
-
-# SmartGatewayUseCount 1
-
-# Determines the take-down percentage for a non-current smart gateway tunnel.
-# If the cost of the current smart gateway tunnel is less than this percentage
-# of the cost of the non-current smart gateway tunnel, then the non-current smart
-# gateway tunnel is taken down because it is then presumed to be 'too expensive'.
-# This setting is only relevant when SmartGatewayUseCount is larger than 1;
-# a value of 0 will result in the tunnels not being taken down proactively.
-# (default is 0)
-
-# SmartGatewayTakeDownPercentage 0
-
-# Determines the policy routing script that is executed during startup and
-# shutdown of olsrd. The script is only executed when SmartGatewayUseCount
-# is set to a value larger than 1. The script must setup policy routing
-# rules such that multi-gateway mode works. A sample script is included.
-# (default is not set)
-
-# SmartGatewayPolicyRoutingScript ""
-
-# Determines the egress interfaces that are part of the multi-gateway setup and
-# therefore only relevant when SmartGatewayUseCount is larger than 1 (in which
-# case it must be explicitly set).
-# (default is not set)
-
-# SmartGatewayEgressInterfaces ""
-
-# Determines the routing tables offset for multi-gateway policy routing tables
-# See the policy routing script for an explanation.
-# (default is 90)
-
-# SmartGatewayTablesOffset 90
-
-# Determines the policy routing rules offset for multi-gateway policy routing
-# rules. See the policy routing script for an explanation.
-# (default is 0, which indicates that the rules and tables should be aligned and
-# puts this value at SmartGatewayTablesOffset - # egress interfaces -
-# # olsr interfaces)
-
-# SmartGatewayRulesOffset 87
-
-# Allows the selection of a smartgateway with NAT (only for IPv4)
-# (default is "yes")
-
-# SmartGatewayAllowNAT yes
-
-# Determines the period (in milliseconds) on which a new smart gateway
-# selection is performed.
-# (default is 10000 milliseconds)
-
-# SmartGatewayPeriod 10000
-
-# Determines the number of times the link state database must be stable
-# before a new smart gateway is selected.
-# (default is 6)
-
-# SmartGatewayStableCount 6
-
-# When another gateway than the current one has a cost of less than the cost
-# of the current gateway multiplied by SmartGatewayThreshold then the smart
-# gateway is switched to the other gateway. The unit is percentage.
-# (defaults to 0)
-
-# SmartGatewayThreshold 0
-
-# The weighing factor for the gateway uplink bandwidth (exit link, uplink).
-# See README-Olsr-Extensions for a description of smart gateways.
-# (default is 1)
-
-# SmartGatewayWeightExitLinkUp 1
-
-# The weighing factor for the gateway downlink bandwidth (exit link, downlink).
-# See README-Olsr-Extensions for a description of smart gateways.
-# (default is 1)
-
-# SmartGatewayWeightExitLinkDown 1
-
-# The weighing factor for the ETX costs.
-# See README-Olsr-Extensions for a description of smart gateways.
-# (default is 1)
-
-# SmartGatewayWeightEtx 1
-
-# The divider for the ETX costs.
-# See README-Olsr-Extensions for a description of smart gateways.
-# (default is 0)
-
-# SmartGatewayDividerEtx 0
-
-# Defines what kind of Uplink this node will publish as a
-# smartgateway. The existence of the uplink is detected by
-# a route to 0.0.0.0/0, ::ffff:0:0/96 and/or 2000::/3.
-# possible values are "none", "ipv4", "ipv6", "both"
-# (default is "both")
-
-# SmartGatewayUplink "both"
-
-# Specifies if the local ipv4 uplink use NAT
-# (default is "yes")
-
-# SmartGatewayUplinkNAT yes
-
-# Specifies the speed of the uplink in kilobit/s.
-# First parameter is upstream, second parameter is downstream
-# (default is 128/1024)
-
-# SmartGatewaySpeed 128 1024
-
-# Specifies the EXTERNAL ipv6 prefix of the uplink. A prefix
-# length of more than 64 is not allowed.
-# (default is 0::/0
-
-# SmartGatewayPrefix 0::/0
-
-##############################
-### OLSR protocol settings ###
-##############################
-
-# HNA (Host network association) allows the OLSR to announce
-# additional IPs or IP subnets to the net that are reachable
-# through this node.
-# Syntax for HNA4 is "network-address    network-mask"
-# Syntax for HNA6 is "network-address    prefix-length"
-# (default is no HNA)
-Hna4
-{
-# Internet gateway
-# 0.0.0.0   0.0.0.0
-# specific small networks reachable through this node
-# 15.15.0.0 255.255.255.0
-}
-Hna6
-{
-# Internet gateway
-#   0::                     0
-# specific small networks reachable through this node
-#   fec0:2200:106:0:0:0:0:0 48
-}
-
-################################
-### OLSR protocol extensions ###
-################################
-
-# Link quality algorithm (only for lq level 2)
-# (see README-Olsr-Extensions)
-# - "etx_float", a floating point  ETX with exponential aging
-# - "etx_fpm", same as ext_float, but with integer arithmetic
-# - "etx_ff" (ETX freifunk), an etx variant which use all OLSR
-#   traffic (instead of only hellos) for ETX calculation
-# - "etx_ffeth", an incompatible variant of etx_ff that allows
-#   ethernet links with ETX 0.1.
-# (defaults to "etx_ff")
-
-# LinkQualityAlgorithm    "etx_ff"
-
-# Fisheye mechanism for TCs (0 meansoff, 1 means on)
-# (default is 1)
-
-LinkQualityFishEye  0
-
-#####################################
-### Example plugin configurations ###
-#####################################
-# Olsrd plugins to load
-# This must be the absolute path to the file
-# or the loader will use the following scheme:
-# - Try the paths in the LD_LIBRARY_PATH
-#   environment variable.
-# - The list of libraries cached in /etc/ld.so.cache
-# - /lib, followed by /usr/lib
-#
-# the examples in this list are for linux, so check if the plugin is
-# available if you use windows.
-# each plugin should have a README file in it's lib subfolder
-
-# LoadPlugin "olsrd_txtinfo.dll"
-#LoadPlugin "olsrd_txtinfo.so.0.1"
-#{
-    # the default port is 2006 but you can change it like this:
-    #PlParam     "port"   "8080"
-
-    # You can set a "accept" single address to allow to connect to
-    # txtinfo. If no address is specified, then localhost (127.0.0.1)
-    # is allowed by default.  txtinfo will only use the first "accept"
-    # parameter specified and will ignore the rest.
-
-    # to allow a specific host:
-    #PlParam      "accept" "172.29.44.23"
-    # if you set it to 0.0.0.0, it will accept all connections
-    #PlParam      "accept" "0.0.0.0"
-#}
-
-#############################################
-### OLSRD default interface configuration ###
-#############################################
-# the default interface section can have the same values as the following
-# interface configuration. It will allow you so set common options for all
-# interfaces.
-
-InterfaceDefaults {
-    Ip4Broadcast      255.255.255.255
-}
-
-######################################
-### OLSRd Interfaces configuration ###
-######################################
-# multiple interfaces can be specified for a single configuration block
-# multiple configuration blocks can be specified
-
-# WARNING, don't forget to insert your interface names here !
-#Interface "<OLSRd-Interface1>" "<OLSRd-Interface2>"
-#{
-    # Interface Mode is used to prevent unnecessary
-    # packet forwarding on switched ethernet interfaces
-    # valid Modes are "mesh" and "ether"
-    # (default is "mesh")
-
-    # Mode "mesh"
-#}
-"""
-        return cfg
-
-
-class MgenActor(NrlService):
-    """
-    ZpcMgenActor.
-    """
-
-    # a unique name is required, without spaces
-    name: str = "MgenActor"
-    group: str = "ProtoSvc"
-    executables: tuple[str, ...] = ("mgen",)
-    configs: tuple[str, ...] = ("start_mgen_actor.sh",)
-    startup: tuple[str, ...] = ("bash start_mgen_actor.sh",)
-    validate: tuple[str, ...] = ("pidof mgen",)
-    shutdown: tuple[str, ...] = ("killall mgen",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a startup script for MgenActor. Because mgenActor does not
-        daemonize, it can cause problems in some situations when launched
-        directly using vcmd.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by nrl.py:MgenActor.generateconfig()\n"
-        comments = ""
-        cmd = f"mgenBasicActor.py -n {node.name} -a 0.0.0.0"
-        ifaces = node.get_ifaces(control=False)
-        if len(ifaces) == 0:
-            return ""
-        cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n"
-        return cfg
diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py
deleted file mode 100644
index b96a8eae4..000000000
--- a/daemon/core/services/quagga.py
+++ /dev/null
@@ -1,584 +0,0 @@
-"""
-quagga.py: defines routing services provided by Quagga.
-"""
-from typing import Optional
-
-import netaddr
-
-from core.emane.nodes import EmaneNet
-from core.nodes.base import CoreNode, NodeBase
-from core.nodes.interface import DEFAULT_MTU, CoreInterface
-from core.nodes.network import PtpNet, WlanNode
-from core.nodes.physical import Rj45Node
-from core.nodes.wireless import WirelessNode
-from core.services.coreservices import CoreService
-
-QUAGGA_STATE_DIR: str = "/var/run/quagga"
-
-
-def is_wireless(node: NodeBase) -> bool:
-    """
-    Check if the node is a wireless type node.
-
-    :param node: node to check type for
-    :return: True if wireless type, False otherwise
-    """
-    return isinstance(node, (WlanNode, EmaneNet, WirelessNode))
-
-
-class Zebra(CoreService):
-    name: str = "zebra"
-    group: str = "Quagga"
-    dirs: tuple[str, ...] = ("/usr/local/etc/quagga", "/var/run/quagga")
-    configs: tuple[str, ...] = (
-        "/usr/local/etc/quagga/Quagga.conf",
-        "quaggaboot.sh",
-        "/usr/local/etc/quagga/vtysh.conf",
-    )
-    startup: tuple[str, ...] = ("bash quaggaboot.sh zebra",)
-    shutdown: tuple[str, ...] = ("killall zebra",)
-    validate: tuple[str, ...] = ("pidof zebra",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the Quagga.conf or quaggaboot.sh file contents.
-        """
-        if filename == cls.configs[0]:
-            return cls.generate_quagga_conf(node)
-        elif filename == cls.configs[1]:
-            return cls.generate_quagga_boot(node)
-        elif filename == cls.configs[2]:
-            return cls.generate_vtysh_conf(node)
-        else:
-            raise ValueError(
-                "file name (%s) is not a known configuration: %s", filename, cls.configs
-            )
-
-    @classmethod
-    def generate_vtysh_conf(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text.
-        """
-        return "service integrated-vtysh-config\n"
-
-    @classmethod
-    def generate_quagga_conf(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text. Other services that depend on zebra
-        will have hooks that are invoked here.
-        """
-        # we could verify here that filename == Quagga.conf
-        cfg = ""
-        for iface in node.get_ifaces():
-            cfg += f"interface {iface.name}\n"
-            # include control interfaces in addressing but not routing daemons
-            if iface.control:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ips()))
-                cfg += "\n"
-                continue
-            cfgv4 = ""
-            cfgv6 = ""
-            want_ipv4 = False
-            want_ipv6 = False
-            for s in node.services:
-                if cls.name not in s.dependencies:
-                    continue
-                if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)):
-                    continue
-                iface_config = s.generate_quagga_iface_config(node, iface)
-                if s.ipv4_routing:
-                    want_ipv4 = True
-                if s.ipv6_routing:
-                    want_ipv6 = True
-                    cfgv6 += iface_config
-                else:
-                    cfgv4 += iface_config
-
-            if want_ipv4:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ip4s))
-                cfg += "\n"
-                cfg += cfgv4
-            if want_ipv6:
-                cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, iface.ip6s))
-                cfg += "\n"
-                cfg += cfgv6
-            cfg += "!\n"
-
-        for s in node.services:
-            if cls.name not in s.dependencies:
-                continue
-            if not (isinstance(s, QuaggaService) or issubclass(s, QuaggaService)):
-                continue
-            cfg += s.generate_quagga_config(node)
-        return cfg
-
-    @staticmethod
-    def addrstr(ip: netaddr.IPNetwork) -> str:
-        """
-        helper for mapping IP addresses to zebra config statements
-        """
-        address = str(ip.ip)
-        if netaddr.valid_ipv4(address):
-            return f"ip address {ip}"
-        elif netaddr.valid_ipv6(address):
-            return f"ipv6 address {ip}"
-        else:
-            raise ValueError(f"invalid address: {ip}")
-
-    @classmethod
-    def generate_quagga_boot(cls, node: CoreNode) -> str:
-        """
-        Generate a shell script used to boot the Quagga daemons.
-        """
-        quagga_bin_search = node.session.options.get(
-            "quagga_bin_search", '"/usr/local/bin /usr/bin /usr/lib/quagga"'
-        )
-        quagga_sbin_search = node.session.options.get(
-            "quagga_sbin_search", '"/usr/local/sbin /usr/sbin /usr/lib/quagga"'
-        )
-        return f"""\
-#!/bin/sh
-# auto-generated by zebra service (quagga.py)
-QUAGGA_CONF={cls.configs[0]}
-QUAGGA_SBIN_SEARCH={quagga_sbin_search}
-QUAGGA_BIN_SEARCH={quagga_bin_search}
-QUAGGA_STATE_DIR={QUAGGA_STATE_DIR}
-
-searchforprog()
-{{
-    prog=$1
-    searchpath=$@
-    ret=
-    for p in $searchpath; do
-        if [ -x $p/$prog ]; then
-            ret=$p
-            break
-        fi
-    done
-    echo $ret
-}}
-
-confcheck()
-{{
-    CONF_DIR=`dirname $QUAGGA_CONF`
-    # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR
-    if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then
-        ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf
-    fi
-    # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR
-    if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then
-        ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf
-    fi
-}}
-
-bootdaemon()
-{{
-    QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH)
-    if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then
-        echo "ERROR: Quagga's '$1' daemon not found in search path:"
-        echo "  $QUAGGA_SBIN_SEARCH"
-        return 1
-    fi
-
-    flags=""
-
-    if [ "$1" = "xpimd" ] && \\
-        grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then
-        flags="$flags -6"
-    fi
-
-    $QUAGGA_SBIN_DIR/$1 $flags -d
-    if [ "$?" != "0" ]; then
-        echo "ERROR: Quagga's '$1' daemon failed to start!:"
-        return 1
-    fi
-}}
-
-bootquagga()
-{{
-    QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH)
-    if [ "z$QUAGGA_BIN_DIR" = "z" ]; then
-        echo "ERROR: Quagga's 'vtysh' program not found in search path:"
-        echo "  $QUAGGA_BIN_SEARCH"
-        return 1
-    fi
-
-    # fix /var/run/quagga permissions
-    id -u quagga 2>/dev/null >/dev/null
-    if [ "$?" = "0" ]; then
-        chown quagga $QUAGGA_STATE_DIR
-    fi
-
-    bootdaemon "zebra"
-    for r in rip ripng ospf6 ospf bgp babel; do
-        if grep -q "^router \\<${{r}}\\>" $QUAGGA_CONF; then
-            bootdaemon "${{r}}d"
-        fi
-    done
-
-    if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then
-        bootdaemon "xpimd"
-    fi
-
-    $QUAGGA_BIN_DIR/vtysh -b
-}}
-
-if [ "$1" != "zebra" ]; then
-    echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!"
-    exit 1
-fi
-confcheck
-bootquagga
-"""
-
-
-class QuaggaService(CoreService):
-    """
-    Parent class for Quagga services. Defines properties and methods
-    common to Quagga's routing daemons.
-    """
-
-    name: Optional[str] = None
-    group: str = "Quagga"
-    dependencies: tuple[str, ...] = (Zebra.name,)
-    meta: str = "The config file for this service can be found in the Zebra service."
-    ipv4_routing: bool = False
-    ipv6_routing: bool = False
-
-    @staticmethod
-    def router_id(node: CoreNode) -> str:
-        """
-        Helper to return the first IPv4 address of a node as its router ID.
-        """
-        for iface in node.get_ifaces(control=False):
-            ip4 = iface.get_ip4()
-            if ip4:
-                return str(ip4.ip)
-        return f"0.0.0.{node.id:d}"
-
-    @staticmethod
-    def rj45check(iface: CoreInterface) -> bool:
-        """
-        Helper to detect whether interface is connected an external RJ45
-        link.
-        """
-        if iface.net:
-            for peer_iface in iface.net.get_ifaces():
-                if peer_iface == iface:
-                    continue
-                if isinstance(peer_iface.node, Rj45Node):
-                    return True
-        return False
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return ""
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        return ""
-
-
-class Ospfv2(QuaggaService):
-    """
-    The OSPFv2 service provides IPv4 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified Quagga.conf file.
-    """
-
-    name: str = "OSPFv2"
-    shutdown: tuple[str, ...] = ("killall ospfd",)
-    validate: tuple[str, ...] = ("pidof ospfd",)
-    ipv4_routing: bool = True
-
-    @staticmethod
-    def mtu_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect MTU mismatch and add the appropriate OSPF
-        mtu-ignore command. This is needed when e.g. a node is linked via a
-        GreTap device.
-        """
-        if iface.mtu != DEFAULT_MTU:
-            # a workaround for PhysicalNode GreTap, which has no knowledge of
-            # the other nodes/nets
-            return "  ip ospf mtu-ignore\n"
-        if not iface.net:
-            return ""
-        for iface in iface.net.get_ifaces():
-            if iface.mtu != iface.mtu:
-                return "  ip ospf mtu-ignore\n"
-        return ""
-
-    @staticmethod
-    def ptp_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect whether interface is connected to a notional
-        point-to-point link.
-        """
-        if isinstance(iface.net, PtpNet):
-            return "  ip ospf network point-to-point\n"
-        return ""
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = "router ospf\n"
-        rtrid = cls.router_id(node)
-        cfg += f"  router-id {rtrid}\n"
-        # network 10.0.0.0/24 area 0
-        for iface in node.get_ifaces(control=False):
-            for ip4 in iface.ip4s:
-                cfg += f"  network {ip4} area 0\n"
-        cfg += "!\n"
-        return cfg
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        cfg = cls.mtu_check(iface)
-        # external RJ45 connections will use default OSPF timers
-        if cls.rj45check(iface):
-            return cfg
-        cfg += cls.ptp_check(iface)
-        return (
-            cfg
-            + """\
-  ip ospf hello-interval 2
-  ip ospf dead-interval 6
-  ip ospf retransmit-interval 5
-"""
-        )
-
-
-class Ospfv3(QuaggaService):
-    """
-    The OSPFv3 service provides IPv6 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified Quagga.conf file.
-    """
-
-    name: str = "OSPFv3"
-    shutdown: tuple[str, ...] = ("killall ospf6d",)
-    validate: tuple[str, ...] = ("pidof ospf6d",)
-    ipv4_routing: bool = True
-    ipv6_routing: bool = True
-
-    @staticmethod
-    def min_mtu(iface: CoreInterface) -> int:
-        """
-        Helper to discover the minimum MTU of interfaces linked with the
-        given interface.
-        """
-        mtu = iface.mtu
-        if not iface.net:
-            return mtu
-        for iface in iface.net.get_ifaces():
-            if iface.mtu < mtu:
-                mtu = iface.mtu
-        return mtu
-
-    @classmethod
-    def mtu_check(cls, iface: CoreInterface) -> str:
-        """
-        Helper to detect MTU mismatch and add the appropriate OSPFv3
-        ifmtu command. This is needed when e.g. a node is linked via a
-        GreTap device.
-        """
-        minmtu = cls.min_mtu(iface)
-        if minmtu < iface.mtu:
-            return f"  ipv6 ospf6 ifmtu {minmtu:d}\n"
-        else:
-            return ""
-
-    @staticmethod
-    def ptp_check(iface: CoreInterface) -> str:
-        """
-        Helper to detect whether interface is connected to a notional
-        point-to-point link.
-        """
-        if isinstance(iface.net, PtpNet):
-            return "  ipv6 ospf6 network point-to-point\n"
-        return ""
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = "router ospf6\n"
-        rtrid = cls.router_id(node)
-        cfg += "  instance-id 65\n"
-        cfg += f"  router-id {rtrid}\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"  interface {iface.name} area 0.0.0.0\n"
-        cfg += "!\n"
-        return cfg
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return cls.mtu_check(iface)
-
-
-class Ospfv3mdr(Ospfv3):
-    """
-    The OSPFv3 MANET Designated Router (MDR) service provides IPv6
-    routing for wireless networks. It does not build its own
-    configuration file but has hooks for adding to the
-    unified Quagga.conf file.
-    """
-
-    name: str = "OSPFv3MDR"
-    ipv4_routing: bool = True
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        cfg = cls.mtu_check(iface)
-        if is_wireless(iface.net):
-            return (
-                cfg
-                + """\
-  ipv6 ospf6 hello-interval 2
-  ipv6 ospf6 dead-interval 6
-  ipv6 ospf6 retransmit-interval 5
-  ipv6 ospf6 network manet-designated-router
-  ipv6 ospf6 twohoprefresh 3
-  ipv6 ospf6 adjacencyconnectivity uniconnected
-  ipv6 ospf6 lsafullness mincostlsa
-"""
-            )
-        else:
-            return cfg
-
-
-class Bgp(QuaggaService):
-    """
-    The BGP service provides interdomain routing.
-    Peers must be manually configured, with a full mesh for those
-    having the same AS number.
-    """
-
-    name: str = "BGP"
-    shutdown: tuple[str, ...] = ("killall bgpd",)
-    validate: tuple[str, ...] = ("pidof bgpd",)
-    custom_needed: bool = True
-    ipv4_routing: bool = True
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = "!\n! BGP configuration\n!\n"
-        cfg += "! You should configure the AS number below,\n"
-        cfg += "! along with this router's peers.\n!\n"
-        cfg += f"router bgp {node.id}\n"
-        rtrid = cls.router_id(node)
-        cfg += f"  bgp router-id {rtrid}\n"
-        cfg += "  redistribute connected\n"
-        cfg += "! neighbor 1.2.3.4 remote-as 555\n!\n"
-        return cfg
-
-
-class Rip(QuaggaService):
-    """
-    The RIP service provides IPv4 routing for wired networks.
-    """
-
-    name: str = "RIP"
-    shutdown: tuple[str, ...] = ("killall ripd",)
-    validate: tuple[str, ...] = ("pidof ripd",)
-    ipv4_routing: bool = True
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = """\
-router rip
-  redistribute static
-  redistribute connected
-  redistribute ospf
-  network 0.0.0.0/0
-!
-"""
-        return cfg
-
-
-class Ripng(QuaggaService):
-    """
-    The RIP NG service provides IPv6 routing for wired networks.
-    """
-
-    name: str = "RIPNG"
-    shutdown: tuple[str, ...] = ("killall ripngd",)
-    validate: tuple[str, ...] = ("pidof ripngd",)
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = """\
-router ripng
-  redistribute static
-  redistribute connected
-  redistribute ospf6
-  network ::/0
-!
-"""
-        return cfg
-
-
-class Babel(QuaggaService):
-    """
-    The Babel service provides a loop-avoiding distance-vector routing
-    protocol for IPv6 and IPv4 with fast convergence properties.
-    """
-
-    name: str = "Babel"
-    shutdown: tuple[str, ...] = ("killall babeld",)
-    validate: tuple[str, ...] = ("pidof babeld",)
-    ipv6_routing: bool = True
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        cfg = "router babel\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"  network {iface.name}\n"
-        cfg += "  redistribute static\n  redistribute connected\n"
-        return cfg
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        if is_wireless(iface.net):
-            return "  babel wireless\n  no babel split-horizon\n"
-        else:
-            return "  babel wired\n  babel split-horizon\n"
-
-
-class Xpimd(QuaggaService):
-    """
-    PIM multicast routing based on XORP.
-    """
-
-    name: str = "Xpimd"
-    shutdown: tuple[str, ...] = ("killall xpimd",)
-    validate: tuple[str, ...] = ("pidof xpimd",)
-    ipv4_routing: bool = True
-
-    @classmethod
-    def generate_quagga_config(cls, node: CoreNode) -> str:
-        ifname = "eth0"
-        for iface in node.get_ifaces():
-            if iface.name != "lo":
-                ifname = iface.name
-                break
-        cfg = "router mfea\n!\n"
-        cfg += "router igmp\n!\n"
-        cfg += "router pim\n"
-        cfg += "  !ip pim rp-address 10.0.0.1\n"
-        cfg += f"  ip pim bsr-candidate {ifname}\n"
-        cfg += f"  ip pim rp-candidate {ifname}\n"
-        cfg += "  !ip pim spt-threshold interval 10 bytes 80000\n"
-        return cfg
-
-    @classmethod
-    def generate_quagga_iface_config(cls, node: CoreNode, iface: CoreInterface) -> str:
-        return "  ip mfea\n  ip igmp\n  ip pim\n"
diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py
deleted file mode 100644
index a31cf87d7..000000000
--- a/daemon/core/services/sdn.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""
-sdn.py defines services to start Open vSwitch and the Ryu SDN Controller.
-"""
-
-import re
-
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService
-
-
-class SdnService(CoreService):
-    """
-    Parent class for SDN services.
-    """
-
-    group: str = "SDN"
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-
-class OvsService(SdnService):
-    name: str = "OvsService"
-    group: str = "SDN"
-    executables: tuple[str, ...] = ("ovs-ofctl", "ovs-vsctl")
-    dirs: tuple[str, ...] = (
-        "/etc/openvswitch",
-        "/var/run/openvswitch",
-        "/var/log/openvswitch",
-    )
-    configs: tuple[str, ...] = ("OvsService.sh",)
-    startup: tuple[str, ...] = ("bash OvsService.sh",)
-    shutdown: tuple[str, ...] = ("killall ovs-vswitchd", "killall ovsdb-server")
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        # Check whether the node is running zebra
-        has_zebra = 0
-        for s in node.services:
-            if s.name == "zebra":
-                has_zebra = 1
-
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by OvsService (OvsService.py)\n"
-        cfg += "## First make sure that the ovs services are up and running\n"
-        cfg += "/etc/init.d/openvswitch-switch start < /dev/null\n\n"
-        cfg += "## create the switch itself, set the fail mode to secure, \n"
-        cfg += "## this stops it from routing traffic without defined flows.\n"
-        cfg += "## remove the -- and everything after if you want it to act as a regular switch\n"
-        cfg += "ovs-vsctl add-br ovsbr0 -- set Bridge ovsbr0 fail-mode=secure\n"
-        cfg += "\n## Now add all our interfaces as ports to the switch\n"
-
-        portnum = 1
-        for iface in node.get_ifaces(control=False):
-            ifnumstr = re.findall(r"\d+", iface.name)
-            ifnum = ifnumstr[0]
-
-            # create virtual interfaces
-            cfg += "## Create a veth pair to send the data to\n"
-            cfg += f"ip link add rtr{ifnum} type veth peer name sw{ifnum}\n"
-
-            # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces
-            # or assign them manually to rtr interfaces if zebra is not running
-            for ip4 in iface.ip4s:
-                cfg += f"ip addr del {ip4.ip} dev {iface.name}\n"
-                if has_zebra == 0:
-                    cfg += f"ip addr add {ip4.ip} dev rtr{ifnum}\n"
-            for ip6 in iface.ip6s:
-                cfg += f"ip -6 addr del {ip6.ip} dev {iface.name}\n"
-                if has_zebra == 0:
-                    cfg += f"ip -6 addr add {ip6.ip} dev rtr{ifnum}\n"
-
-            # add interfaces to bridge
-            # Make port numbers explicit so they're easier to follow in
-            # reading the script
-            cfg += "## Add the CORE interface to the switch\n"
-            cfg += (
-                f"ovs-vsctl add-port ovsbr0 eth{ifnum} -- "
-                f"set Interface eth{ifnum} ofport_request={portnum:d}\n"
-            )
-            cfg += "## And then add its sibling veth interface\n"
-            cfg += (
-                f"ovs-vsctl add-port ovsbr0 sw{ifnum} -- "
-                f"set Interface sw{ifnum} ofport_request={portnum + 1:d}\n"
-            )
-            cfg += "## start them up so we can send/receive data\n"
-            cfg += f"ovs-ofctl mod-port ovsbr0 eth{ifnum} up\n"
-            cfg += f"ovs-ofctl mod-port ovsbr0 sw{ifnum} up\n"
-            cfg += "## Bring up the lower part of the veth pair\n"
-            cfg += f"ip link set dev rtr{ifnum} up\n"
-            portnum += 2
-
-        # Add rule for default controller if there is one local
-        # (even if the controller is not local, it finds it)
-        cfg += "\n## We assume there will be an SDN controller on the other end of this, \n"
-        cfg += "## but it will still function if there's not\n"
-        cfg += "ovs-vsctl set-controller ovsbr0 tcp:127.0.0.1:6633\n"
-
-        cfg += "\n## Now to create some default flows, \n"
-        cfg += "## if the above controller will be present then you probably want to delete them\n"
-        # Setup default flows
-        portnum = 1
-        for iface in node.get_ifaces(control=False):
-            cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n"
-            cfg += f"ovs-ofctl add-flow ovsbr0 priority=1000,in_port={portnum:d},action=output:{portnum + 1:d}\n"
-            cfg += f"ovs-ofctl add-flow ovsbr0 priority=1000,in_port={portnum + 1:d},action=output:{portnum:d}\n"
-            portnum += 2
-        return cfg
-
-
-class RyuService(SdnService):
-    name: str = "ryuService"
-    group: str = "SDN"
-    executables: tuple[str, ...] = ("ryu-manager",)
-    configs: tuple[str, ...] = ("ryuService.sh",)
-    startup: tuple[str, ...] = ("bash ryuService.sh",)
-    shutdown: tuple[str, ...] = ("killall ryu-manager",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return a string that will be written to filename, or sent to the
-        GUI for user customization.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by ryuService (ryuService.py)\n"
-        cfg += (
-            "ryu-manager --observe-links ryu.app.ofctl_rest ryu.app.rest_topology &\n"
-        )
-        return cfg
diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py
deleted file mode 100644
index afd71a140..000000000
--- a/daemon/core/services/security.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""
-security.py: defines security services (vpnclient, vpnserver, ipsec and
-firewall)
-"""
-
-import logging
-
-from core import constants
-from core.nodes.base import CoreNode
-from core.nodes.interface import CoreInterface
-from core.services.coreservices import CoreService
-
-logger = logging.getLogger(__name__)
-
-
-class VPNClient(CoreService):
-    name: str = "VPNClient"
-    group: str = "Security"
-    configs: tuple[str, ...] = ("vpnclient.sh",)
-    startup: tuple[str, ...] = ("bash vpnclient.sh",)
-    shutdown: tuple[str, ...] = ("killall openvpn",)
-    validate: tuple[str, ...] = ("pidof openvpn",)
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the client.conf and vpnclient.sh file contents to
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# custom VPN Client configuration for service (security.py)\n"
-        fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNClient"
-        try:
-            with open(fname) as f:
-                cfg += f.read()
-        except OSError:
-            logger.exception(
-                "error opening VPN client configuration template (%s)", fname
-            )
-        return cfg
-
-
-class VPNServer(CoreService):
-    name: str = "VPNServer"
-    group: str = "Security"
-    configs: tuple[str, ...] = ("vpnserver.sh",)
-    startup: tuple[str, ...] = ("bash vpnserver.sh",)
-    shutdown: tuple[str, ...] = ("killall openvpn",)
-    validate: tuple[str, ...] = ("pidof openvpn",)
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the sample server.conf and vpnserver.sh file contents to
-        GUI for user customization.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# custom VPN Server Configuration for service (security.py)\n"
-        fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleVPNServer"
-        try:
-            with open(fname) as f:
-                cfg += f.read()
-        except OSError:
-            logger.exception(
-                "Error opening VPN server configuration template (%s)", fname
-            )
-        return cfg
-
-
-class IPsec(CoreService):
-    name: str = "IPsec"
-    group: str = "Security"
-    configs: tuple[str, ...] = ("ipsec.sh",)
-    startup: tuple[str, ...] = ("bash ipsec.sh",)
-    shutdown: tuple[str, ...] = ("killall racoon",)
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the ipsec.conf and racoon.conf file contents to
-        GUI for user customization.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# set up static tunnel mode security assocation for service "
-        cfg += "(security.py)\n"
-        fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleIPsec"
-        try:
-            with open(fname) as f:
-                cfg += f.read()
-        except OSError:
-            logger.exception("Error opening IPsec configuration template (%s)", fname)
-        return cfg
-
-
-class Firewall(CoreService):
-    name: str = "Firewall"
-    group: str = "Security"
-    configs: tuple[str, ...] = ("firewall.sh",)
-    startup: tuple[str, ...] = ("bash firewall.sh",)
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the firewall rule examples to GUI for user customization.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# custom node firewall rules for service (security.py)\n"
-        fname = f"{constants.CORE_DATA_DIR}/examples/services/sampleFirewall"
-        try:
-            with open(fname) as f:
-                cfg += f.read()
-        except OSError:
-            logger.exception(
-                "Error opening Firewall configuration template (%s)", fname
-            )
-        return cfg
-
-
-class Nat(CoreService):
-    """
-    IPv4 source NAT service.
-    """
-
-    name: str = "NAT"
-    group: str = "Security"
-    executables: tuple[str, ...] = ("iptables",)
-    configs: tuple[str, ...] = ("nat.sh",)
-    startup: tuple[str, ...] = ("bash nat.sh",)
-    custom_needed: bool = False
-
-    @classmethod
-    def generate_iface_nat_rule(cls, iface: CoreInterface, prefix: str = "") -> str:
-        """
-        Generate a NAT line for one interface.
-        """
-        cfg = prefix + "iptables -t nat -A POSTROUTING -o "
-        cfg += iface.name + " -j MASQUERADE\n"
-        cfg += prefix + "iptables -A FORWARD -i " + iface.name
-        cfg += " -m state --state RELATED,ESTABLISHED -j ACCEPT\n"
-        cfg += prefix + "iptables -A FORWARD -i "
-        cfg += iface.name + " -j DROP\n"
-        return cfg
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        NAT out the first interface
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# generated by security.py\n"
-        cfg += "# NAT out the first interface by default\n"
-        have_nat = False
-        for iface in node.get_ifaces(control=False):
-            if have_nat:
-                cfg += cls.generate_iface_nat_rule(iface, prefix="#")
-            else:
-                have_nat = True
-                cfg += "# NAT out the " + iface.name + " interface\n"
-                cfg += cls.generate_iface_nat_rule(iface)
-                cfg += "\n"
-        return cfg
diff --git a/daemon/core/services/ucarp.py b/daemon/core/services/ucarp.py
deleted file mode 100644
index c6f2256ec..000000000
--- a/daemon/core/services/ucarp.py
+++ /dev/null
@@ -1,165 +0,0 @@
-"""
-ucarp.py: defines high-availability IP address controlled by ucarp
-"""
-
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService
-
-UCARP_ETC = "/usr/local/etc/ucarp"
-
-
-class Ucarp(CoreService):
-    name: str = "ucarp"
-    group: str = "Utility"
-    dirs: tuple[str, ...] = (UCARP_ETC,)
-    configs: tuple[str, ...] = (
-        UCARP_ETC + "/default.sh",
-        UCARP_ETC + "/default-up.sh",
-        UCARP_ETC + "/default-down.sh",
-        "ucarpboot.sh",
-    )
-    startup: tuple[str, ...] = ("bash ucarpboot.sh",)
-    shutdown: tuple[str, ...] = ("killall ucarp",)
-    validate: tuple[str, ...] = ("pidof ucarp",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Return the default file contents
-        """
-        if filename == cls.configs[0]:
-            return cls.generate_ucarp_conf(node)
-        elif filename == cls.configs[1]:
-            return cls.generate_vip_up(node)
-        elif filename == cls.configs[2]:
-            return cls.generate_vip_down(node)
-        elif filename == cls.configs[3]:
-            return cls.generate_ucarp_boot(node)
-        else:
-            raise ValueError
-
-    @classmethod
-    def generate_ucarp_conf(cls, node: CoreNode) -> str:
-        """
-        Returns configuration file text.
-        """
-        ucarp_bin = node.session.options.get("ucarp_bin", "/usr/sbin/ucarp")
-        return f"""\
-#!/bin/sh
-# Location of UCARP executable
-UCARP_EXEC={ucarp_bin}
-
-# Location of the UCARP config directory
-UCARP_CFGDIR={UCARP_ETC}
-
-# Logging Facility
-FACILITY=daemon
-
-# Instance ID
-# Any number from 1 to 255
-INSTANCE_ID=1
-
-# Password
-# Master and Backup(s) need to be the same
-PASSWORD="changeme"
-
-# The failover application address
-VIRTUAL_ADDRESS=127.0.0.254
-VIRTUAL_NET=8
-
-# Interface for IP Address
-INTERFACE=lo
-
-# Maintanence address of the local machine
-SOURCE_ADDRESS=127.0.0.1
-
-# The ratio number to be considered before marking the node as dead
-DEAD_RATIO=3
-
-# UCARP base, lower number will be preferred master
-# set to same to have master stay as long as possible
-UCARP_BASE=1
-SKEW=0
-
-# UCARP options
-# -z run shutdown script on exit
-# -P force preferred master
-# -n don't run down script at start up when we are backup
-# -M use broadcast instead of multicast
-# -S ignore interface state
-OPTIONS="-z -n -M"
-
-# Send extra parameter to down and up scripts
-#XPARAM="-x <enter param here>"
-XPARAM="-x ${{VIRTUAL_NET}}"
-
-# The start and stop scripts
-START_SCRIPT=${{UCARP_CFGDIR}}/default-up.sh
-STOP_SCRIPT=${{UCARP_CFGDIR}}/default-down.sh
-
-# These line should not need to be touched
-UCARP_OPTS="$OPTIONS -b $UCARP_BASE -k $SKEW -i $INTERFACE -v $INSTANCE_ID -p $PASSWORD -u $START_SCRIPT -d $STOP_SCRIPT -a $VIRTUAL_ADDRESS -s $SOURCE_ADDRESS -f $FACILITY $XPARAM"
-
-${{UCARP_EXEC}} -B ${{UCARP_OPTS}}
-"""
-
-    @classmethod
-    def generate_ucarp_boot(cls, node: CoreNode) -> str:
-        """
-        Generate a shell script used to boot the Ucarp daemons.
-        """
-        return f"""\
-#!/bin/sh
-# Location of the UCARP config directory
-UCARP_CFGDIR={UCARP_ETC}
-
-chmod a+x ${{UCARP_CFGDIR}}/*.sh
-
-# Start the default ucarp daemon configuration
-${{UCARP_CFGDIR}}/default.sh
-
-"""
-
-    @classmethod
-    def generate_vip_up(cls, node: CoreNode) -> str:
-        """
-        Generate a shell script used to start the virtual ip
-        """
-        return """\
-#!/bin/bash
-
-# Should be invoked as "default-up.sh <dev> <ip>"
-exec 2> /dev/null
-
-IP="${2}"
-NET="${3}"
-if [ -z "$NET" ]; then
-    NET="24"
-fi
-
-/sbin/ip addr add ${IP}/${NET} dev "$1"
-
-
-"""
-
-    @classmethod
-    def generate_vip_down(cls, node: CoreNode) -> str:
-        """
-        Generate a shell script used to stop the virtual ip
-        """
-        return """\
-#!/bin/bash
-
-# Should be invoked as "default-down.sh <dev> <ip>"
-exec 2> /dev/null
-
-IP="${2}"
-NET="${3}"
-if [ -z "$NET" ]; then
-    NET="24"
-fi
-
-/sbin/ip addr del ${IP}/${NET} dev "$1"
-
-
-"""
diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py
deleted file mode 100644
index e83cb9d56..000000000
--- a/daemon/core/services/utility.py
+++ /dev/null
@@ -1,665 +0,0 @@
-"""
-utility.py: defines miscellaneous utility services.
-"""
-from typing import Optional
-
-import netaddr
-
-from core import utils
-from core.errors import CoreCommandError
-from core.executables import SYSCTL
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService, ServiceMode
-
-
-class UtilService(CoreService):
-    """
-    Parent class for utility services.
-    """
-
-    name: Optional[str] = None
-    group: str = "Utility"
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-
-class IPForwardService(UtilService):
-    name: str = "IPForward"
-    configs: tuple[str, ...] = ("ipforward.sh",)
-    startup: tuple[str, ...] = ("bash ipforward.sh",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return cls.generateconfiglinux(node, filename)
-
-    @classmethod
-    def generateconfiglinux(cls, node: CoreNode, filename: str) -> str:
-        cfg = f"""\
-#!/bin/sh
-# auto-generated by IPForward service (utility.py)
-{SYSCTL} -w net.ipv4.conf.all.forwarding=1
-{SYSCTL} -w net.ipv4.conf.default.forwarding=1
-{SYSCTL} -w net.ipv6.conf.all.forwarding=1
-{SYSCTL} -w net.ipv6.conf.default.forwarding=1
-{SYSCTL} -w net.ipv4.conf.all.send_redirects=0
-{SYSCTL} -w net.ipv4.conf.default.send_redirects=0
-{SYSCTL} -w net.ipv4.conf.all.rp_filter=0
-{SYSCTL} -w net.ipv4.conf.default.rp_filter=0
-"""
-        for iface in node.get_ifaces():
-            name = utils.sysctl_devname(iface.name)
-            cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.forwarding=1\n"
-            cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.send_redirects=0\n"
-            cfg += f"{SYSCTL} -w net.ipv4.conf.{name}.rp_filter=0\n"
-        return cfg
-
-
-class DefaultRouteService(UtilService):
-    name: str = "DefaultRoute"
-    configs: tuple[str, ...] = ("defaultroute.sh",)
-    startup: tuple[str, ...] = ("bash defaultroute.sh",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        routes = []
-        ifaces = node.get_ifaces()
-        if ifaces:
-            iface = ifaces[0]
-            for ip in iface.ips():
-                net = ip.cidr
-                if net.size > 1:
-                    router = net[1]
-                    routes.append(str(router))
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by DefaultRoute service (utility.py)\n"
-        for route in routes:
-            cfg += f"ip route add default via {route}\n"
-        return cfg
-
-
-class DefaultMulticastRouteService(UtilService):
-    name: str = "DefaultMulticastRoute"
-    configs: tuple[str, ...] = ("defaultmroute.sh",)
-    startup: tuple[str, ...] = ("bash defaultmroute.sh",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by DefaultMulticastRoute service (utility.py)\n"
-        cfg += "# the first interface is chosen below; please change it "
-        cfg += "as needed\n"
-        for iface in node.get_ifaces(control=False):
-            rtcmd = "ip route add 224.0.0.0/4 dev"
-            cfg += f"{rtcmd} {iface.name}\n"
-            cfg += "\n"
-            break
-        return cfg
-
-
-class StaticRouteService(UtilService):
-    name: str = "StaticRoute"
-    configs: tuple[str, ...] = ("staticroute.sh",)
-    startup: tuple[str, ...] = ("bash staticroute.sh",)
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by StaticRoute service (utility.py)\n#\n"
-        cfg += "# NOTE: this service must be customized to be of any use\n"
-        cfg += "#       Below are samples that you can uncomment and edit.\n#\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += "\n".join(map(cls.routestr, iface.ips()))
-            cfg += "\n"
-        return cfg
-
-    @staticmethod
-    def routestr(ip: netaddr.IPNetwork) -> str:
-        address = str(ip.ip)
-        if netaddr.valid_ipv6(address):
-            dst = "3ffe:4::/64"
-        else:
-            dst = "10.9.8.0/24"
-        if ip[-2] == ip[1]:
-            return ""
-        else:
-            rtcmd = f"#/sbin/ip route add {dst} via"
-            return f"{rtcmd} {ip[1]}"
-
-
-class SshService(UtilService):
-    name: str = "SSH"
-    configs: tuple[str, ...] = ("startsshd.sh", "/etc/ssh/sshd_config")
-    dirs: tuple[str, ...] = ("/etc/ssh", "/var/run/sshd")
-    startup: tuple[str, ...] = ("bash startsshd.sh",)
-    shutdown: tuple[str, ...] = ("killall sshd",)
-    validation_mode: ServiceMode = ServiceMode.BLOCKING
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Use a startup script for launching sshd in order to wait for host
-        key generation.
-        """
-        sshcfgdir = cls.dirs[0]
-        sshstatedir = cls.dirs[1]
-        sshlibdir = "/usr/lib/openssh"
-        if filename == "startsshd.sh":
-            return f"""\
-#!/bin/sh
-# auto-generated by SSH service (utility.py)
-ssh-keygen -q -t rsa -N "" -f {sshcfgdir}/ssh_host_rsa_key
-chmod 655 {sshstatedir}
-# wait until RSA host key has been generated to launch sshd
-/usr/sbin/sshd -f {sshcfgdir}/sshd_config
-"""
-        else:
-            return f"""\
-# auto-generated by SSH service (utility.py)
-Port 22
-Protocol 2
-HostKey {sshcfgdir}/ssh_host_rsa_key
-UsePrivilegeSeparation yes
-PidFile {sshstatedir}/sshd.pid
-
-KeyRegenerationInterval 3600
-ServerKeyBits 768
-
-SyslogFacility AUTH
-LogLevel INFO
-
-LoginGraceTime 120
-PermitRootLogin yes
-StrictModes yes
-
-RSAAuthentication yes
-PubkeyAuthentication yes
-
-IgnoreRhosts yes
-RhostsRSAAuthentication no
-HostbasedAuthentication no
-
-PermitEmptyPasswords no
-ChallengeResponseAuthentication no
-
-X11Forwarding yes
-X11DisplayOffset 10
-PrintMotd no
-PrintLastLog yes
-TCPKeepAlive yes
-
-AcceptEnv LANG LC_*
-Subsystem sftp {sshlibdir}/sftp-server
-UsePAM yes
-UseDNS no
-"""
-
-
-class DhcpService(UtilService):
-    name: str = "DHCP"
-    configs: tuple[str, ...] = ("/etc/dhcp/dhcpd.conf",)
-    dirs: tuple[str, ...] = ("/etc/dhcp", "/var/lib/dhcp")
-    startup: tuple[str, ...] = ("touch /var/lib/dhcp/dhcpd.leases", "dhcpd")
-    shutdown: tuple[str, ...] = ("killall dhcpd",)
-    validate: tuple[str, ...] = ("pidof dhcpd",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a dhcpd config file using the network address of
-        each interface.
-        """
-        cfg = """\
-# auto-generated by DHCP service (utility.py)
-# NOTE: move these option lines into the desired pool { } block(s) below
-#option domain-name "test.com";
-#option domain-name-servers 10.0.0.1;
-#option routers 10.0.0.1;
-
-log-facility local6;
-
-default-lease-time 600;
-max-lease-time 7200;
-
-ddns-update-style none;
-"""
-        for iface in node.get_ifaces(control=False):
-            cfg += "\n".join(map(cls.subnetentry, iface.ip4s))
-            cfg += "\n"
-        return cfg
-
-    @staticmethod
-    def subnetentry(ip: netaddr.IPNetwork) -> str:
-        """
-        Generate a subnet declaration block given an IPv4 prefix string
-        for inclusion in the dhcpd3 config file.
-        """
-        if ip.size == 1:
-            return ""
-        # divide the address space in half
-        index = (ip.size - 2) / 2
-        rangelow = ip[index]
-        rangehigh = ip[-2]
-        return f"""
-subnet {ip.cidr.ip} netmask {ip.netmask} {{
-  pool {{
-    range {rangelow} {rangehigh};
-    default-lease-time 600;
-    option routers {ip.ip};
-  }}
-}}
-"""
-
-
-class DhcpClientService(UtilService):
-    """
-    Use a DHCP client for all interfaces for addressing.
-    """
-
-    name: str = "DHCPClient"
-    configs: tuple[str, ...] = ("startdhcpclient.sh",)
-    startup: tuple[str, ...] = ("bash startdhcpclient.sh",)
-    shutdown: tuple[str, ...] = ("killall dhclient",)
-    validate: tuple[str, ...] = ("pidof dhclient",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a script to invoke dhclient on all interfaces.
-        """
-        cfg = "#!/bin/sh\n"
-        cfg += "# auto-generated by DHCPClient service (utility.py)\n"
-        cfg += "# uncomment this mkdir line and symlink line to enable client-"
-        cfg += "side DNS\n# resolution based on the DHCP server response.\n"
-        cfg += "#mkdir -p /var/run/resolvconf/interface\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"#ln -s /var/run/resolvconf/interface/{iface.name}.dhclient"
-            cfg += " /var/run/resolvconf/resolv.conf\n"
-            cfg += f"/sbin/dhclient -nw -pf /var/run/dhclient-{iface.name}.pid"
-            cfg += f" -lf /var/run/dhclient-{iface.name}.lease {iface.name}\n"
-        return cfg
-
-
-class FtpService(UtilService):
-    """
-    Start a vsftpd server.
-    """
-
-    name: str = "FTP"
-    configs: tuple[str, ...] = ("vsftpd.conf",)
-    dirs: tuple[str, ...] = ("/var/run/vsftpd/empty", "/var/ftp")
-    startup: tuple[str, ...] = ("vsftpd ./vsftpd.conf",)
-    shutdown: tuple[str, ...] = ("killall vsftpd",)
-    validate: tuple[str, ...] = ("pidof vsftpd",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a vsftpd.conf configuration file.
-        """
-        return """\
-# vsftpd.conf auto-generated by FTP service (utility.py)
-listen=YES
-anonymous_enable=YES
-local_enable=YES
-dirmessage_enable=YES
-use_localtime=YES
-xferlog_enable=YES
-connect_from_port_20=YES
-xferlog_file=/var/log/vsftpd.log
-ftpd_banner=Welcome to the CORE FTP service
-secure_chroot_dir=/var/run/vsftpd/empty
-anon_root=/var/ftp
-"""
-
-
-class HttpService(UtilService):
-    """
-    Start an apache server.
-    """
-
-    name: str = "HTTP"
-    configs: tuple[str, ...] = (
-        "/etc/apache2/apache2.conf",
-        "/etc/apache2/envvars",
-        "/var/www/index.html",
-    )
-    dirs: tuple[str, ...] = (
-        "/etc/apache2",
-        "/var/run/apache2",
-        "/var/log/apache2",
-        "/run/lock",
-        "/var/lock/apache2",
-        "/var/www",
-    )
-    startup: tuple[str, ...] = ("chown www-data /var/lock/apache2", "apache2ctl start")
-    shutdown: tuple[str, ...] = ("apache2ctl stop",)
-    validate: tuple[str, ...] = ("pidof apache2",)
-    APACHEVER22: int = 22
-    APACHEVER24: int = 24
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate an apache2.conf configuration file.
-        """
-        if filename == cls.configs[0]:
-            return cls.generateapache2conf(node, filename)
-        elif filename == cls.configs[1]:
-            return cls.generateenvvars(node, filename)
-        elif filename == cls.configs[2]:
-            return cls.generatehtml(node, filename)
-        else:
-            return ""
-
-    @classmethod
-    def detectversionfromcmd(cls) -> int:
-        """
-        Detect the apache2 version using the 'a2query' command.
-        """
-        try:
-            result = utils.cmd("a2query -v")
-            status = 0
-        except CoreCommandError as e:
-            status = e.returncode
-            result = e.stderr
-        if status == 0 and result[:3] == "2.4":
-            return cls.APACHEVER24
-        return cls.APACHEVER22
-
-    @classmethod
-    def generateapache2conf(cls, node: CoreNode, filename: str) -> str:
-        lockstr = {
-            cls.APACHEVER22: "LockFile ${APACHE_LOCK_DIR}/accept.lock\n",
-            cls.APACHEVER24: "Mutex file:${APACHE_LOCK_DIR} default\n",
-        }
-        mpmstr = {
-            cls.APACHEVER22: "",
-            cls.APACHEVER24: "LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so\n",
-        }
-        permstr = {
-            cls.APACHEVER22: "    Order allow,deny\n    Deny from all\n    Satisfy all\n",
-            cls.APACHEVER24: "    Require all denied\n",
-        }
-        authstr = {
-            cls.APACHEVER22: "LoadModule authz_default_module /usr/lib/apache2/modules/mod_authz_default.so\n",
-            cls.APACHEVER24: "LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so\n",
-        }
-        permstr2 = {
-            cls.APACHEVER22: "\t\tOrder allow,deny\n\t\tallow from all\n",
-            cls.APACHEVER24: "\t\tRequire all granted\n",
-        }
-        version = cls.detectversionfromcmd()
-        cfg = "# apache2.conf generated by utility.py:HttpService\n"
-        cfg += lockstr[version]
-        cfg += """\
-PidFile ${APACHE_PID_FILE}
-Timeout 300
-KeepAlive On
-MaxKeepAliveRequests 100
-KeepAliveTimeout 5
-"""
-        cfg += mpmstr[version]
-        cfg += """\
-
-<IfModule mpm_prefork_module>
-    StartServers          5
-    MinSpareServers       5
-    MaxSpareServers      10
-    MaxClients          150
-    MaxRequestsPerChild   0
-</IfModule>
-
-<IfModule mpm_worker_module>
-    StartServers          2
-    MinSpareThreads      25
-    MaxSpareThreads      75
-    ThreadLimit          64
-    ThreadsPerChild      25
-    MaxClients          150
-    MaxRequestsPerChild   0
-</IfModule>
-
-<IfModule mpm_event_module>
-    StartServers          2
-    MinSpareThreads      25
-    MaxSpareThreads      75
-    ThreadLimit          64
-    ThreadsPerChild      25
-    MaxClients          150
-    MaxRequestsPerChild   0
-</IfModule>
-
-User ${APACHE_RUN_USER}
-Group ${APACHE_RUN_GROUP}
-
-AccessFileName .htaccess
-
-<Files ~ "^\\.ht">
-"""
-        cfg += permstr[version]
-        cfg += """\
-</Files>
-
-DefaultType None
-
-HostnameLookups Off
-
-ErrorLog ${APACHE_LOG_DIR}/error.log
-LogLevel warn
-
-#Include mods-enabled/*.load
-#Include mods-enabled/*.conf
-LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
-LoadModule auth_basic_module /usr/lib/apache2/modules/mod_auth_basic.so
-"""
-        cfg += authstr[version]
-        cfg += """\
-LoadModule authz_host_module /usr/lib/apache2/modules/mod_authz_host.so
-LoadModule authz_user_module /usr/lib/apache2/modules/mod_authz_user.so
-LoadModule autoindex_module /usr/lib/apache2/modules/mod_autoindex.so
-LoadModule dir_module /usr/lib/apache2/modules/mod_dir.so
-LoadModule env_module /usr/lib/apache2/modules/mod_env.so
-
-NameVirtualHost *:80
-Listen 80
-
-<IfModule mod_ssl.c>
-    Listen 443
-</IfModule>
-<IfModule mod_gnutls.c>
-    Listen 443
-</IfModule>
-
-LogFormat "%v:%p %h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" vhost_combined
-LogFormat "%h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined
-LogFormat "%h %l %u %t \\"%r\\" %>s %O" common
-LogFormat "%{Referer}i -> %U" referer
-LogFormat "%{User-agent}i" agent
-
-ServerTokens OS
-ServerSignature On
-TraceEnable Off
-
-<VirtualHost *:80>
-    ServerAdmin webmaster@localhost
-    DocumentRoot /var/www
-    <Directory />
-        Options FollowSymLinks
-        AllowOverride None
-    </Directory>
-    <Directory /var/www/>
-        Options Indexes FollowSymLinks MultiViews
-        AllowOverride None
-"""
-        cfg += permstr2[version]
-        cfg += """\
-    </Directory>
-    ErrorLog ${APACHE_LOG_DIR}/error.log
-    LogLevel warn
-    CustomLog ${APACHE_LOG_DIR}/access.log combined
-</VirtualHost>
-
-"""
-        return cfg
-
-    @classmethod
-    def generateenvvars(cls, node: CoreNode, filename: str) -> str:
-        return """\
-# this file is used by apache2ctl - generated by utility.py:HttpService
-# these settings come from a default Ubuntu apache2 installation
-export APACHE_RUN_USER=www-data
-export APACHE_RUN_GROUP=www-data
-export APACHE_PID_FILE=/var/run/apache2.pid
-export APACHE_RUN_DIR=/var/run/apache2
-export APACHE_LOCK_DIR=/var/lock/apache2
-export APACHE_LOG_DIR=/var/log/apache2
-export LANG=C
-export LANG
-"""
-
-    @classmethod
-    def generatehtml(cls, node: CoreNode, filename: str) -> str:
-        body = f"""\
-<!-- generated by utility.py:HttpService -->
-<h1>{node.name} web server</h1>
-<p>This is the default web page for this server.</p>
-<p>The web server software is running but no content has been added, yet.</p>
-"""
-        for iface in node.get_ifaces(control=False):
-            body += f"<li>{iface.name} - {[str(x) for x in iface.ips()]}</li>\n"
-        return f"<html><body>{body}</body></html>"
-
-
-class PcapService(UtilService):
-    """
-    Pcap service for logging packets.
-    """
-
-    name: str = "pcap"
-    configs: tuple[str, ...] = ("pcap.sh",)
-    startup: tuple[str, ...] = ("bash pcap.sh start",)
-    shutdown: tuple[str, ...] = ("bash pcap.sh stop",)
-    validate: tuple[str, ...] = ("pidof tcpdump",)
-    meta: str = "logs network traffic to pcap packet capture files"
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a startpcap.sh traffic logging script.
-        """
-        cfg = """
-#!/bin/sh
-# set tcpdump options here (see 'man tcpdump' for help)
-# (-s snap length, -C limit pcap file length, -n disable name resolution)
-DUMPOPTS="-s 12288 -C 10 -n"
-
-if [ "x$1" = "xstart" ]; then
-
-"""
-        for iface in node.get_ifaces():
-            if iface.control:
-                cfg += "# "
-            redir = "< /dev/null"
-            cfg += (
-                f"tcpdump ${{DUMPOPTS}} -w {node.name}.{iface.name}.pcap "
-                f"-i {iface.name} {redir} &\n"
-            )
-        cfg += """
-
-elif [ "x$1" = "xstop" ]; then
-    mkdir -p ${SESSION_DIR}/pcap
-    mv *.pcap ${SESSION_DIR}/pcap
-fi;
-"""
-        return cfg
-
-
-class RadvdService(UtilService):
-    name: str = "radvd"
-    configs: tuple[str, ...] = ("/etc/radvd/radvd.conf",)
-    dirs: tuple[str, ...] = ("/etc/radvd", "/var/run/radvd")
-    startup: tuple[str, ...] = (
-        "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log",
-    )
-    shutdown: tuple[str, ...] = ("pkill radvd",)
-    validate: tuple[str, ...] = ("pidof radvd",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Generate a RADVD router advertisement daemon config file
-        using the network address of each interface.
-        """
-        cfg = "# auto-generated by RADVD service (utility.py)\n"
-        for iface in node.get_ifaces(control=False):
-            prefixes = list(map(cls.subnetentry, iface.ips()))
-            if len(prefixes) < 1:
-                continue
-            cfg += f"""\
-interface {iface.name}
-{{
-        AdvSendAdvert on;
-        MinRtrAdvInterval 3;
-        MaxRtrAdvInterval 10;
-        AdvDefaultPreference low;
-        AdvHomeAgentFlag off;
-"""
-            for prefix in prefixes:
-                if prefix == "":
-                    continue
-                cfg += f"""\
-        prefix {prefix}
-        {{
-                AdvOnLink on;
-                AdvAutonomous on;
-                AdvRouterAddr on;
-        }};
-"""
-            cfg += "};\n"
-        return cfg
-
-    @staticmethod
-    def subnetentry(ip: netaddr.IPNetwork) -> str:
-        """
-        Generate a subnet declaration block given an IPv6 prefix string
-        for inclusion in the RADVD config file.
-        """
-        address = str(ip.ip)
-        if netaddr.valid_ipv6(address):
-            return str(ip)
-        else:
-            return ""
-
-
-class AtdService(UtilService):
-    """
-    Atd service for scheduling at jobs
-    """
-
-    name: str = "atd"
-    configs: tuple[str, ...] = ("startatd.sh",)
-    dirs: tuple[str, ...] = ("/var/spool/cron/atjobs", "/var/spool/cron/atspool")
-    startup: tuple[str, ...] = ("bash startatd.sh",)
-    shutdown: tuple[str, ...] = ("pkill atd",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return """
-#!/bin/sh
-echo 00001 > /var/spool/cron/atjobs/.SEQ
-chown -R daemon /var/spool/cron/*
-chmod -R 700 /var/spool/cron/*
-atd
-"""
-
-
-class UserDefinedService(UtilService):
-    """
-    Dummy service allowing customization of anything.
-    """
-
-    name: str = "UserDefined"
-    meta: str = "Customize this service to do anything upon startup."
diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py
deleted file mode 100644
index ac29b2999..000000000
--- a/daemon/core/services/xorp.py
+++ /dev/null
@@ -1,436 +0,0 @@
-"""
-xorp.py: defines routing services provided by the XORP routing suite.
-"""
-
-from typing import Optional
-
-import netaddr
-
-from core.nodes.base import CoreNode
-from core.nodes.interface import CoreInterface
-from core.services.coreservices import CoreService
-
-
-class XorpRtrmgr(CoreService):
-    """
-    XORP router manager service builds a config.boot file based on other
-    enabled XORP services, and launches necessary daemons upon startup.
-    """
-
-    name: str = "xorp_rtrmgr"
-    group: str = "XORP"
-    executables: tuple[str, ...] = ("xorp_rtrmgr",)
-    dirs: tuple[str, ...] = ("/etc/xorp",)
-    configs: tuple[str, ...] = ("/etc/xorp/config.boot",)
-    startup: tuple[
-        str, ...
-    ] = f"xorp_rtrmgr -d -b {configs[0]} -l /var/log/{name}.log -P /var/run/{name}.pid"
-    shutdown: tuple[str, ...] = ("killall xorp_rtrmgr",)
-    validate: tuple[str, ...] = ("pidof xorp_rtrmgr",)
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Returns config.boot configuration file text. Other services that
-        depend on this will have generatexorpconfig() hooks that are
-        invoked here. Filename currently ignored.
-        """
-        cfg = "interfaces {\n"
-        for iface in node.get_ifaces():
-            cfg += f"    interface {iface.name} {{\n"
-            cfg += f"\tvif {iface.name} {{\n"
-            cfg += "".join(map(cls.addrstr, iface.ips()))
-            cfg += cls.lladdrstr(iface)
-            cfg += "\t}\n"
-            cfg += "    }\n"
-        cfg += "}\n\n"
-
-        for s in node.services:
-            if cls.name not in s.dependencies:
-                continue
-            if not (isinstance(s, XorpService) or issubclass(s, XorpService)):
-                continue
-            cfg += s.generate_xorp_config(node)
-        return cfg
-
-    @staticmethod
-    def addrstr(ip: netaddr.IPNetwork) -> str:
-        """
-        helper for mapping IP addresses to XORP config statements
-        """
-        cfg = f"\t    address {ip.ip} {{\n"
-        cfg += f"\t\tprefix-length: {ip.prefixlen}\n"
-        cfg += "\t    }\n"
-        return cfg
-
-    @staticmethod
-    def lladdrstr(iface: CoreInterface) -> str:
-        """
-        helper for adding link-local address entries (required by OSPFv3)
-        """
-        cfg = f"\t    address {iface.mac.eui64()} {{\n"
-        cfg += "\t\tprefix-length: 64\n"
-        cfg += "\t    }\n"
-        return cfg
-
-
-class XorpService(CoreService):
-    """
-    Parent class for XORP services. Defines properties and methods
-    common to XORP's routing daemons.
-    """
-
-    name: Optional[str] = None
-    group: str = "XORP"
-    executables: tuple[str, ...] = ("xorp_rtrmgr",)
-    dependencies: tuple[str, ...] = ("xorp_rtrmgr",)
-    meta: str = (
-        "The config file for this service can be found in the xorp_rtrmgr service."
-    )
-
-    @staticmethod
-    def fea(forwarding: str) -> str:
-        """
-        Helper to add a forwarding engine entry to the config file.
-        """
-        cfg = "fea {\n"
-        cfg += f"    {forwarding} {{\n"
-        cfg += "\tdisable:false\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-    @staticmethod
-    def mfea(forwarding, node: CoreNode) -> str:
-        """
-        Helper to add a multicast forwarding engine entry to the config file.
-        """
-        names = []
-        for iface in node.get_ifaces(control=False):
-            names.append(iface.name)
-        names.append("register_vif")
-        cfg = "plumbing {\n"
-        cfg += f"    {forwarding} {{\n"
-        for name in names:
-            cfg += f"\tinterface {name} {{\n"
-            cfg += f"\t    vif {name} {{\n"
-            cfg += "\t\tdisable: false\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-    @staticmethod
-    def policyexportconnected() -> str:
-        """
-        Helper to add a policy statement for exporting connected routes.
-        """
-        cfg = "policy {\n"
-        cfg += "    policy-statement export-connected {\n"
-        cfg += "\tterm 100 {\n"
-        cfg += "\t    from {\n"
-        cfg += '\t\tprotocol: "connected"\n'
-        cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-    @staticmethod
-    def router_id(node: CoreNode) -> str:
-        """
-        Helper to return the first IPv4 address of a node as its router ID.
-        """
-        for iface in node.get_ifaces(control=False):
-            ip4 = iface.get_ip4()
-            if ip4:
-                return str(ip4.ip)
-        return "0.0.0.0"
-
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        return ""
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        return ""
-
-
-class XorpOspfv2(XorpService):
-    """
-    The OSPFv2 service provides IPv4 routing for wired networks. It does
-    not build its own configuration file but has hooks for adding to the
-    unified XORP configuration file.
-    """
-
-    name: str = "XORP_OSPFv2"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.fea("unicast-forwarding4")
-        rtrid = cls.router_id(node)
-        cfg += "\nprotocols {\n"
-        cfg += "    ospf4 {\n"
-        cfg += f"\trouter-id: {rtrid}\n"
-        cfg += "\tarea 0.0.0.0 {\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"\t    interface {iface.name} {{\n"
-            cfg += f"\t\tvif {iface.name} {{\n"
-            for ip4 in iface.ip4s:
-                cfg += f"\t\t    address {ip4.ip} {{\n"
-                cfg += "\t\t    }\n"
-            cfg += "\t\t}\n"
-            cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpOspfv3(XorpService):
-    """
-    The OSPFv3 service provides IPv6 routing. It does
-    not build its own configuration file but has hooks for adding to the
-    unified XORP configuration file.
-    """
-
-    name: str = "XORP_OSPFv3"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.fea("unicast-forwarding6")
-        rtrid = cls.router_id(node)
-        cfg += "\nprotocols {\n"
-        cfg += "    ospf6 0 { /* Instance ID 0 */\n"
-        cfg += f"\trouter-id: {rtrid}\n"
-        cfg += "\tarea 0.0.0.0 {\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"\t    interface {iface.name} {{\n"
-            cfg += f"\t\tvif {iface.name} {{\n"
-            cfg += "\t\t}\n"
-            cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpBgp(XorpService):
-    """
-    IPv4 inter-domain routing. AS numbers and peers must be customized.
-    """
-
-    name: str = "XORP_BGP"
-    custom_needed: bool = True
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = "/* This is a sample config that should be customized with\n"
-        cfg += " appropriate AS numbers and peers */\n"
-        cfg += cls.fea("unicast-forwarding4")
-        cfg += cls.policyexportconnected()
-        rtrid = cls.router_id(node)
-        cfg += "\nprotocols {\n"
-        cfg += "    bgp {\n"
-        cfg += f"\tbgp-id: {rtrid}\n"
-        cfg += "\tlocal-as: 65001 /* change this */\n"
-        cfg += '\texport: "export-connected"\n'
-        cfg += "\tpeer 10.0.1.1 { /* change this */\n"
-        cfg += "\t    local-ip: 10.0.1.1\n"
-        cfg += "\t    as: 65002\n"
-        cfg += "\t    next-hop: 10.0.0.2\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpRip(XorpService):
-    """
-    RIP IPv4 unicast routing.
-    """
-
-    name: str = "XORP_RIP"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.fea("unicast-forwarding4")
-        cfg += cls.policyexportconnected()
-        cfg += "\nprotocols {\n"
-        cfg += "    rip {\n"
-        cfg += '\texport: "export-connected"\n'
-        for iface in node.get_ifaces(control=False):
-            cfg += f"\tinterface {iface.name} {{\n"
-            cfg += f"\t    vif {iface.name} {{\n"
-            for ip4 in iface.ip4s:
-                cfg += f"\t\taddress {ip4.ip} {{\n"
-                cfg += "\t\t    disable: false\n"
-                cfg += "\t\t}\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpRipng(XorpService):
-    """
-    RIP NG IPv6 unicast routing.
-    """
-
-    name: str = "XORP_RIPNG"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.fea("unicast-forwarding6")
-        cfg += cls.policyexportconnected()
-        cfg += "\nprotocols {\n"
-        cfg += "    ripng {\n"
-        cfg += '\texport: "export-connected"\n'
-        for iface in node.get_ifaces(control=False):
-            cfg += f"\tinterface {iface.name} {{\n"
-            cfg += f"\t    vif {iface.name} {{\n"
-            cfg += f"\t\taddress {iface.mac.eui64()} {{\n"
-            cfg += "\t\t    disable: false\n"
-            cfg += "\t\t}\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpPimSm4(XorpService):
-    """
-    PIM Sparse Mode IPv4 multicast routing.
-    """
-
-    name: str = "XORP_PIMSM4"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.mfea("mfea4", node)
-        cfg += "\nprotocols {\n"
-        cfg += "    igmp {\n"
-        names = []
-        for iface in node.get_ifaces(control=False):
-            names.append(iface.name)
-            cfg += f"\tinterface {iface.name} {{\n"
-            cfg += f"\t    vif {iface.name} {{\n"
-            cfg += "\t\tdisable: false\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        cfg += "\nprotocols {\n"
-        cfg += "    pimsm4 {\n"
-
-        names.append("register_vif")
-        for name in names:
-            cfg += f"\tinterface {name} {{\n"
-            cfg += f"\t    vif {name} {{\n"
-            cfg += "\t\tdr-priority: 1\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "\tbootstrap {\n"
-        cfg += "\t    cand-bsr {\n"
-        cfg += "\t\tscope-zone 224.0.0.0/4 {\n"
-        cfg += f'\t\t    cand-bsr-by-vif-name: "{names[0]}"\n'
-        cfg += "\t\t}\n"
-        cfg += "\t    }\n"
-        cfg += "\t    cand-rp {\n"
-        cfg += "\t\tgroup-prefix 224.0.0.0/4 {\n"
-        cfg += f'\t\t    cand-rp-by-vif-name: "{names[0]}"\n'
-        cfg += "\t\t}\n"
-        cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        cfg += "\nprotocols {\n"
-        cfg += "    fib2mrib {\n"
-        cfg += "\tdisable: false\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpPimSm6(XorpService):
-    """
-    PIM Sparse Mode IPv6 multicast routing.
-    """
-
-    name: str = "XORP_PIMSM6"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.mfea("mfea6", node)
-        cfg += "\nprotocols {\n"
-        cfg += "    mld {\n"
-        names = []
-        for iface in node.get_ifaces(control=False):
-            names.append(iface.name)
-            cfg += f"\tinterface {iface.name} {{\n"
-            cfg += f"\t    vif {iface.name} {{\n"
-            cfg += "\t\tdisable: false\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        cfg += "\nprotocols {\n"
-        cfg += "    pimsm6 {\n"
-
-        names.append("register_vif")
-        for name in names:
-            cfg += f"\tinterface {name} {{\n"
-            cfg += f"\t    vif {name} {{\n"
-            cfg += "\t\tdr-priority: 1\n"
-            cfg += "\t    }\n"
-            cfg += "\t}\n"
-        cfg += "\tbootstrap {\n"
-        cfg += "\t    cand-bsr {\n"
-        cfg += "\t\tscope-zone ff00::/8 {\n"
-        cfg += f'\t\t    cand-bsr-by-vif-name: "{names[0]}"\n'
-        cfg += "\t\t}\n"
-        cfg += "\t    }\n"
-        cfg += "\t    cand-rp {\n"
-        cfg += "\t\tgroup-prefix ff00::/8 {\n"
-        cfg += f'\t\t    cand-rp-by-vif-name: "{names[0]}"\n'
-        cfg += "\t\t}\n"
-        cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        cfg += "\nprotocols {\n"
-        cfg += "    fib2mrib {\n"
-        cfg += "\tdisable: false\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
-
-
-class XorpOlsr(XorpService):
-    """
-    OLSR IPv4 unicast MANET routing.
-    """
-
-    name: str = "XORP_OLSR"
-
-    @classmethod
-    def generate_xorp_config(cls, node: CoreNode) -> str:
-        cfg = cls.fea("unicast-forwarding4")
-        rtrid = cls.router_id(node)
-        cfg += "\nprotocols {\n"
-        cfg += "    olsr4 {\n"
-        cfg += f"\tmain-address: {rtrid}\n"
-        for iface in node.get_ifaces(control=False):
-            cfg += f"\tinterface {iface.name} {{\n"
-            cfg += f"\t    vif {iface.name} {{\n"
-            for ip4 in iface.ip4s:
-                cfg += f"\t\taddress {ip4.ip} {{\n"
-                cfg += "\t\t}\n"
-            cfg += "\t    }\n"
-        cfg += "\t}\n"
-        cfg += "    }\n"
-        cfg += "}\n"
-        return cfg
diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py
index 6d6c4940a..cfea8d696 100644
--- a/daemon/core/xml/corexml.py
+++ b/daemon/core/xml/corexml.py
@@ -19,7 +19,6 @@
 from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode
 from core.nodes.podman import PodmanNode, PodmanOptions
 from core.nodes.wireless import WirelessNode
-from core.services.coreservices import CoreService
 
 logger = logging.getLogger(__name__)
 
@@ -148,68 +147,6 @@ def add_position(self) -> None:
         add_attribute(position, "alt", alt)
 
 
-class ServiceElement:
-    def __init__(self, service: type[CoreService]) -> None:
-        self.service: type[CoreService] = service
-        self.element: etree.Element = etree.Element("service")
-        add_attribute(self.element, "name", service.name)
-        self.add_directories()
-        self.add_startup()
-        self.add_validate()
-        self.add_shutdown()
-        self.add_files()
-
-    def add_directories(self) -> None:
-        # get custom directories
-        directories = etree.Element("directories")
-        for directory in self.service.dirs:
-            directory_element = etree.SubElement(directories, "directory")
-            directory_element.text = directory
-
-        if directories.getchildren():
-            self.element.append(directories)
-
-    def add_files(self) -> None:
-        file_elements = etree.Element("files")
-        for file_name in self.service.config_data:
-            data = self.service.config_data[file_name]
-            file_element = etree.SubElement(file_elements, "file")
-            add_attribute(file_element, "name", file_name)
-            file_element.text = etree.CDATA(data)
-        if file_elements.getchildren():
-            self.element.append(file_elements)
-
-    def add_startup(self) -> None:
-        # get custom startup
-        startup_elements = etree.Element("startups")
-        for startup in self.service.startup:
-            startup_element = etree.SubElement(startup_elements, "startup")
-            startup_element.text = startup
-
-        if startup_elements.getchildren():
-            self.element.append(startup_elements)
-
-    def add_validate(self) -> None:
-        # get custom validate
-        validate_elements = etree.Element("validates")
-        for validate in self.service.validate:
-            validate_element = etree.SubElement(validate_elements, "validate")
-            validate_element.text = validate
-
-        if validate_elements.getchildren():
-            self.element.append(validate_elements)
-
-    def add_shutdown(self) -> None:
-        # get custom shutdown
-        shutdown_elements = etree.Element("shutdowns")
-        for shutdown in self.service.shutdown:
-            shutdown_element = etree.SubElement(shutdown_elements, "shutdown")
-            shutdown_element.text = shutdown
-
-        if shutdown_elements.getchildren():
-            self.element.append(shutdown_elements)
-
-
 class DeviceElement(NodeElement):
     def __init__(self, session: "Session", node: NodeBase) -> None:
         super().__init__(session, node, "device")
@@ -234,8 +171,6 @@ def add_class(self) -> None:
 
     def add_services(self) -> None:
         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)
 
@@ -290,7 +225,6 @@ def write_session(self) -> None:
         self.write_links()
         self.write_mobility_configs()
         self.write_emane_configs()
-        self.write_service_configs()
         self.write_configservice_configs()
         self.write_session_origin()
         self.write_servers()
@@ -413,17 +347,6 @@ def write_mobility_configs(self) -> None:
         if mobility_configurations.getchildren():
             self.scenario.append(mobility_configurations)
 
-    def write_service_configs(self) -> None:
-        service_configurations = etree.Element("service_configurations")
-        service_configs = self.session.services.all_configs()
-        for node_id, service in service_configs:
-            service_element = ServiceElement(service)
-            add_attribute(service_element.element, "node", node_id)
-            service_configurations.append(service_element.element)
-
-        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():
@@ -452,8 +375,7 @@ def write_configservice_configs(self) -> None:
 
     def write_default_services(self) -> None:
         models = etree.Element("default_services")
-        for model in self.session.services.default_services:
-            services = self.session.services.default_services[model]
+        for model, services in []:
             model = etree.SubElement(models, "node", type=model)
             for service in services:
                 etree.SubElement(model, "service", name=service)
@@ -585,7 +507,6 @@ def read(self, file_path: Path) -> None:
         self.read_session_hooks()
         self.read_servers()
         self.read_session_origin()
-        self.read_service_configs()
         self.read_mobility_configs()
         self.read_nodes()
         self.read_links()
@@ -603,7 +524,6 @@ def read_default_services(self) -> None:
             for service in node.iterchildren():
                 services.append(service.get("name"))
             logger.info("reading default services for nodes(%s): %s", model, services)
-            self.session.services.default_services[model] = services
 
     def read_session_metadata(self) -> None:
         session_metadata = self.scenario.find("session_metadata")
@@ -677,50 +597,6 @@ def read_session_origin(self) -> None:
             logger.info("reading session reference xyz: %s, %s, %s", x, y, z)
             self.session.location.refxyz = (x, y, z)
 
-    def read_service_configs(self) -> None:
-        service_configurations = self.scenario.find("service_configurations")
-        if service_configurations is None:
-            return
-
-        for service_configuration in service_configurations.iterchildren():
-            node_id = get_int(service_configuration, "node")
-            service_name = service_configuration.get("name")
-            logger.info(
-                "reading custom service(%s) for node(%s)", service_name, node_id
-            )
-            self.session.services.set_service(node_id, service_name)
-            service = self.session.services.get_service(node_id, service_name)
-
-            directory_elements = service_configuration.find("directories")
-            if directory_elements is not None:
-                service.dirs = tuple(x.text for x in directory_elements.iterchildren())
-
-            startup_elements = service_configuration.find("startups")
-            if startup_elements is not None:
-                service.startup = tuple(x.text for x in startup_elements.iterchildren())
-
-            validate_elements = service_configuration.find("validates")
-            if validate_elements is not None:
-                service.validate = tuple(
-                    x.text for x in validate_elements.iterchildren()
-                )
-
-            shutdown_elements = service_configuration.find("shutdowns")
-            if shutdown_elements is not None:
-                service.shutdown = tuple(
-                    x.text for x in shutdown_elements.iterchildren()
-                )
-
-            file_elements = service_configuration.find("files")
-            if file_elements is not None:
-                files = set(service.configs)
-                for file_element in file_elements.iterchildren():
-                    name = file_element.get("name")
-                    data = file_element.text
-                    service.config_data[name] = data
-                    files.add(name)
-                service.configs = tuple(files)
-
     def read_emane_configs(self) -> None:
         emane_configurations = self.scenario.find("emane_configurations")
         if emane_configurations is None:
diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto
index 14cb84338..e6f48def0 100644
--- a/daemon/proto/core/api/grpc/core.proto
+++ b/daemon/proto/core/api/grpc/core.proto
@@ -72,18 +72,6 @@ service CoreApi {
     rpc MobilityAction (mobility.MobilityActionRequest) returns (mobility.MobilityActionResponse) {
     }
 
-    // service rpc
-    rpc GetServiceDefaults (services.GetServiceDefaultsRequest) returns (services.GetServiceDefaultsResponse) {
-    }
-    rpc SetServiceDefaults (services.SetServiceDefaultsRequest) returns (services.SetServiceDefaultsResponse) {
-    }
-    rpc GetNodeService (services.GetNodeServiceRequest) returns (services.GetNodeServiceResponse) {
-    }
-    rpc GetNodeServiceFile (services.GetNodeServiceFileRequest) returns (services.GetNodeServiceFileResponse) {
-    }
-    rpc ServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) {
-    }
-
     // config services
     rpc GetConfigServiceDefaults (configservices.GetConfigServiceDefaultsRequest) returns (configservices.GetConfigServiceDefaultsResponse) {
     }
@@ -146,9 +134,8 @@ message GetConfigRequest {
 }
 
 message GetConfigResponse {
-    repeated services.Service services = 1;
-    repeated configservices.ConfigService config_services = 2;
-    repeated string emane_models = 3;
+    repeated configservices.ConfigService config_services = 1;
+    repeated string emane_models = 2;
 }
 
 
@@ -585,22 +572,20 @@ message Node {
     NodeType.Enum type = 3;
     string model = 4;
     Position position = 5;
-    repeated string services = 6;
-    string emane = 7;
-    string icon = 8;
-    string image = 9;
-    string server = 10;
-    repeated string config_services = 11;
-    Geo geo = 12;
-    string dir = 13;
-    string channel = 14;
-    int32 canvas = 15;
-    map<string, common.ConfigOption> wlan_config = 16;
-    map<string, common.ConfigOption> mobility_config = 17;
-    map<string, services.NodeServiceConfig> service_configs = 18;
-    map<string, configservices.ConfigServiceConfig> config_service_configs= 19;
-    repeated emane.NodeEmaneConfig emane_configs = 20;
-    map<string, common.ConfigOption> wireless_config = 21;
+    string emane = 6;
+    string icon = 7;
+    string image = 8;
+    string server = 9;
+    repeated string config_services = 10;
+    Geo geo = 11;
+    string dir = 12;
+    string channel = 13;
+    int32 canvas = 14;
+    map<string, common.ConfigOption> wlan_config = 15;
+    map<string, common.ConfigOption> mobility_config = 16;
+    map<string, configservices.ConfigServiceConfig> config_service_configs= 17;
+    repeated emane.NodeEmaneConfig emane_configs = 18;
+    map<string, common.ConfigOption> wireless_config = 19;
 }
 
 message Link {
diff --git a/daemon/proto/core/api/grpc/services.proto b/daemon/proto/core/api/grpc/services.proto
index 1b430f99e..c88efe2ac 100644
--- a/daemon/proto/core/api/grpc/services.proto
+++ b/daemon/proto/core/api/grpc/services.proto
@@ -2,31 +2,6 @@ syntax = "proto3";
 
 package services;
 
-message ServiceConfig {
-    int32 node_id = 1;
-    string service = 2;
-    repeated string startup = 3;
-    repeated string validate = 4;
-    repeated string shutdown = 5;
-    repeated string files = 6;
-    repeated string directories = 7;
-}
-
-message ServiceFileConfig {
-    int32 node_id = 1;
-    string service = 2;
-    string file = 3;
-    string data = 4;
-}
-
-message ServiceValidationMode {
-    enum Enum {
-        BLOCKING = 0;
-        NON_BLOCKING = 1;
-        TIMER = 2;
-    }
-}
-
 message ServiceAction {
     enum Enum {
         START = 0;
@@ -41,69 +16,6 @@ message ServiceDefaults {
     repeated string services = 2;
 }
 
-message Service {
-    string group = 1;
-    string name = 2;
-}
-
-message NodeServiceData {
-    repeated string executables = 1;
-    repeated string dependencies = 2;
-    repeated string dirs = 3;
-    repeated string configs = 4;
-    repeated string startup = 5;
-    repeated string validate = 6;
-    ServiceValidationMode.Enum validation_mode = 7;
-    int32 validation_timer = 8;
-    repeated string shutdown = 9;
-    string meta = 10;
-}
-
-message NodeServiceConfig {
-    int32 node_id = 1;
-    string service = 2;
-    NodeServiceData data = 3;
-    map<string, string> files = 4;
-}
-
-message GetServiceDefaultsRequest {
-    int32 session_id = 1;
-}
-
-message GetServiceDefaultsResponse {
-    repeated ServiceDefaults defaults = 1;
-}
-
-message SetServiceDefaultsRequest {
-    int32 session_id = 1;
-    repeated ServiceDefaults defaults = 2;
-}
-
-message SetServiceDefaultsResponse {
-    bool result = 1;
-}
-
-message GetNodeServiceRequest {
-    int32 session_id = 1;
-    int32 node_id = 2;
-    string service = 3;
-}
-
-message GetNodeServiceResponse {
-    NodeServiceData service = 1;
-}
-
-message GetNodeServiceFileRequest {
-    int32 session_id = 1;
-    int32 node_id = 2;
-    string service = 3;
-    string file = 4;
-}
-
-message GetNodeServiceFileResponse {
-    string data = 1;
-}
-
 message ServiceActionRequest {
     int32 session_id = 1;
     int32 node_id = 2;
diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py
index 250cbfc5b..ded03d317 100644
--- a/daemon/tests/test_grpc.py
+++ b/daemon/tests/test_grpc.py
@@ -14,6 +14,7 @@
 from core.api.grpc.wrappers import (
     ConfigOption,
     ConfigOptionType,
+    ConfigServiceData,
     EmaneModelConfig,
     Event,
     Geo,
@@ -24,14 +25,13 @@
     MobilityAction,
     MoveNodesRequest,
     Node,
-    NodeServiceData,
     NodeType,
     Position,
     ServiceAction,
-    ServiceValidationMode,
     SessionLocation,
     SessionState,
 )
+from core.configservices.utilservices.services import DefaultRouteService
 from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
 from core.emulator.data import EventData, IpPrefixes, NodeData
@@ -93,25 +93,13 @@ def test_start_session(self, grpc_server: CoreGrpcServer, definition):
         wlan_node.set_mobility({mobility_config_key: mobility_config_value})
 
         # setup service config
-        service_name = "DefaultRoute"
-        service_validate = ["echo hello"]
-        node1.service_configs[service_name] = NodeServiceData(
-            executables=[],
-            dependencies=[],
-            dirs=[],
-            configs=[],
-            startup=[],
-            validate=service_validate,
-            validation_mode=ServiceValidationMode.NON_BLOCKING,
-            validation_timer=0,
-            shutdown=[],
-            meta="",
+        service_name = DefaultRouteService.name
+        file_name = DefaultRouteService.files[0]
+        file_data = "hello world"
+        service_data = ConfigServiceData(
+            templates={file_name: file_data},
         )
-
-        # setup service file config
-        service_file = "defaultroute.sh"
-        service_file_data = "echo hello"
-        node1.service_file_configs[service_name] = {service_file: service_file_data}
+        node1.config_service_configs[service_name] = service_data
 
         # setup session option
         option_key = "controlnet"
@@ -153,16 +141,11 @@ def test_start_session(self, grpc_server: CoreGrpcServer, definition):
             wlan_node.id, Ns2ScriptedMobility.name
         )
         assert set_mobility_config[mobility_config_key] == mobility_config_value
-        service = real_session.services.get_service(
-            node1.id, service_name, default_service=True
-        )
-        assert service.validate == tuple(service_validate)
         real_node1 = real_session.get_node(node1.id, CoreNode)
-        service_file = real_session.services.get_service_file(
-            real_node1, service_name, service_file
-        )
-        assert service_file.data == service_file_data
-        assert option_value == real_session.options.get(option_key)
+        real_service = real_node1.config_services[service_name]
+        real_templates = real_service.get_templates()
+        real_template_data = real_templates[file_name]
+        assert file_data == real_template_data
 
     @pytest.mark.parametrize("session_id", [None, 6013])
     def test_create_session(
@@ -628,79 +611,6 @@ def test_mobility_action(self, grpc_server: CoreGrpcServer):
         # then
         assert result is True
 
-    def test_get_service_defaults(self, grpc_server: CoreGrpcServer):
-        # given
-        client = CoreGrpcClient()
-        session = grpc_server.coreemu.create_session()
-
-        # then
-        with client.context_connect():
-            defaults = client.get_service_defaults(session.id)
-
-        # then
-        assert len(defaults) > 0
-
-    def test_set_service_defaults(self, grpc_server: CoreGrpcServer):
-        # given
-        client = CoreGrpcClient()
-        session = grpc_server.coreemu.create_session()
-        model = "test"
-        services = ["SSH"]
-
-        # then
-        with client.context_connect():
-            result = client.set_service_defaults(session.id, {model: services})
-
-        # then
-        assert result is True
-        assert session.services.default_services[model] == services
-
-    def test_get_node_service(self, grpc_server: CoreGrpcServer):
-        # given
-        client = CoreGrpcClient()
-        session = grpc_server.coreemu.create_session()
-        node = session.add_node(CoreNode)
-
-        # then
-        with client.context_connect():
-            service = client.get_node_service(session.id, node.id, "DefaultRoute")
-
-        # then
-        assert len(service.configs) > 0
-
-    def test_get_node_service_file(self, grpc_server: CoreGrpcServer):
-        # given
-        client = CoreGrpcClient()
-        session = grpc_server.coreemu.create_session()
-        node = session.add_node(CoreNode)
-
-        # then
-        with client.context_connect():
-            data = client.get_node_service_file(
-                session.id, node.id, "DefaultRoute", "defaultroute.sh"
-            )
-
-        # then
-        assert data is not None
-
-    def test_service_action(self, grpc_server: CoreGrpcServer):
-        # given
-        client = CoreGrpcClient()
-        session = grpc_server.coreemu.create_session()
-        options = CoreNode.create_options()
-        options.legacy = True
-        node = session.add_node(CoreNode, options=options)
-        service_name = "DefaultRoute"
-
-        # then
-        with client.context_connect():
-            result = client.service_action(
-                session.id, node.id, service_name, ServiceAction.STOP
-            )
-
-        # then
-        assert result is True
-
     def test_config_service_action(self, grpc_server: CoreGrpcServer):
         # given
         client = CoreGrpcClient()
diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py
deleted file mode 100644
index 69234e3a7..000000000
--- a/daemon/tests/test_services.py
+++ /dev/null
@@ -1,376 +0,0 @@
-import itertools
-from pathlib import Path
-
-import pytest
-from mock import MagicMock
-
-from core.emulator.session import Session
-from core.errors import CoreCommandError
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager
-
-_PATH: Path = Path(__file__).resolve().parent
-_SERVICES_PATH = _PATH / "myservices"
-
-SERVICE_ONE = "MyService"
-SERVICE_TWO = "MyService2"
-
-
-class TestServices:
-    def test_service_all_files(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        file_name = "myservice.sh"
-        node = session.add_node(CoreNode)
-
-        # when
-        session.services.set_service_file(node.id, SERVICE_ONE, file_name, "# test")
-
-        # then
-        service = session.services.get_service(node.id, SERVICE_ONE)
-        all_files = session.services.all_files(service)
-        assert service
-        assert all_files and len(all_files) == 1
-
-    def test_service_all_configs(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        node = session.add_node(CoreNode)
-
-        # when
-        session.services.set_service(node.id, SERVICE_ONE)
-        session.services.set_service(node.id, SERVICE_TWO)
-
-        # then
-        all_configs = session.services.all_configs()
-        assert all_configs
-        assert len(all_configs) == 2
-
-    def test_service_add_services(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        node = session.add_node(CoreNode)
-        total_service = len(node.services)
-
-        # when
-        session.services.add_services(node, node.model, [SERVICE_ONE, SERVICE_TWO])
-
-        # then
-        assert node.services
-        assert len(node.services) == total_service + 2
-
-    def test_service_file(self, request, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-        file_path = Path(my_service.configs[0])
-        file_path = node.host_path(file_path)
-
-        # when
-        session.services.create_service_files(node, my_service)
-
-        # then
-        if not request.config.getoption("mock"):
-            assert file_path.exists()
-
-    def test_service_validate(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-
-        # when
-        status = session.services.validate_service(node, my_service)
-
-        # then
-        assert not status
-
-    def test_service_validate_error(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_TWO)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-        node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid"))
-
-        # when
-        status = session.services.validate_service(node, my_service)
-
-        # then
-        assert status
-
-    def test_service_startup(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-
-        # when
-        status = session.services.startup_service(node, my_service, wait=True)
-
-        # then
-        assert not status
-
-    def test_service_startup_error(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_TWO)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-        node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid"))
-
-        # when
-        status = session.services.startup_service(node, my_service, wait=True)
-
-        # then
-        assert status
-
-    def test_service_stop(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-
-        # when
-        status = session.services.stop_service(node, my_service)
-
-        # then
-        assert not status
-
-    def test_service_stop_error(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_TWO)
-        node = session.add_node(CoreNode)
-        session.services.create_service_files(node, my_service)
-        node.cmd = MagicMock(side_effect=CoreCommandError(-1, "invalid"))
-
-        # when
-        status = session.services.stop_service(node, my_service)
-
-        # then
-        assert status
-
-    def test_service_custom_startup(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-
-        # when
-        session.services.set_service(node.id, my_service.name)
-        custom_my_service = session.services.get_service(node.id, my_service.name)
-        custom_my_service.startup = ("sh custom.sh",)
-
-        # then
-        assert my_service.startup != custom_my_service.startup
-
-    def test_service_set_file(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node1 = session.add_node(CoreNode)
-        node2 = session.add_node(CoreNode)
-        file_name = my_service.configs[0]
-        file_data1 = "# custom file one"
-        file_data2 = "# custom file two"
-        session.services.set_service_file(
-            node1.id, my_service.name, file_name, file_data1
-        )
-        session.services.set_service_file(
-            node2.id, my_service.name, file_name, file_data2
-        )
-
-        # when
-        custom_service1 = session.services.get_service(node1.id, my_service.name)
-        session.services.create_service_files(node1, custom_service1)
-        custom_service2 = session.services.get_service(node2.id, my_service.name)
-        session.services.create_service_files(node2, custom_service2)
-
-    def test_service_import(self):
-        """
-        Test importing a custom service.
-        """
-        ServiceManager.add_services(_SERVICES_PATH)
-        assert ServiceManager.get(SERVICE_ONE)
-        assert ServiceManager.get(SERVICE_TWO)
-
-    def test_service_setget(self, session: Session):
-        # given
-        ServiceManager.add_services(_SERVICES_PATH)
-        my_service = ServiceManager.get(SERVICE_ONE)
-        node = session.add_node(CoreNode)
-
-        # when
-        no_service = session.services.get_service(node.id, SERVICE_ONE)
-        default_service = session.services.get_service(
-            node.id, SERVICE_ONE, default_service=True
-        )
-        session.services.set_service(node.id, SERVICE_ONE)
-        custom_service = session.services.get_service(
-            node.id, SERVICE_ONE, default_service=True
-        )
-
-        # then
-        assert no_service is None
-        assert default_service == my_service
-        assert custom_service and custom_service != my_service
-
-    def test_services_dependency(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_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
-        permutations = itertools.permutations(services)
-        for permutation in permutations:
-            permutation = list(permutation)
-            results = ServiceDependencies(permutation).boot_order()
-            # then
-            for result in results:
-                result_set = {x.name for x in result}
-                if len(result) == 4:
-                    a_index = result.index(service_a)
-                    b_index = result.index(service_b)
-                    c_index = result.index(service_c)
-                    d_index = result.index(service_d)
-                    assert b_index < a_index
-                    assert b_index < c_index
-                    assert d_index < c_index
-                    assert result_set == expected1
-                elif len(result) == 1:
-                    assert expected2 == result
-                else:
-                    raise ValueError(
-                        f"unexpected result: {results}, perm({permutation})"
-                    )
-
-    def test_services_dependency_missing(self):
-        # given
-        service_a = CoreService()
-        service_a.name = "a"
-        service_b = CoreService()
-        service_b.name = "b"
-        service_c = CoreService()
-        service_c.name = "c"
-        service_a.dependencies = (service_b.name,)
-        service_b.dependencies = (service_c.name,)
-        service_c.dependencies = ("d",)
-        services = [service_a, service_b, service_c]
-
-        # when, then
-        permutations = itertools.permutations(services)
-        for permutation in permutations:
-            permutation = list(permutation)
-            with pytest.raises(ValueError):
-                ServiceDependencies(permutation).boot_order()
-
-    def test_services_dependency_cycle(self):
-        # given
-        service_a = CoreService()
-        service_a.name = "a"
-        service_b = CoreService()
-        service_b.name = "b"
-        service_c = CoreService()
-        service_c.name = "c"
-        service_a.dependencies = (service_b.name,)
-        service_b.dependencies = (service_c.name,)
-        service_c.dependencies = (service_a.name,)
-        services = [service_a, service_b, service_c]
-
-        # when, then
-        permutations = itertools.permutations(services)
-        for permutation in permutations:
-            permutation = list(permutation)
-            with pytest.raises(ValueError):
-                ServiceDependencies(permutation).boot_order()
-
-    def test_services_dependency_common(self):
-        # given
-        service_a = CoreService()
-        service_a.name = "a"
-        service_b = CoreService()
-        service_b.name = "b"
-        service_c = CoreService()
-        service_c.name = "c"
-        service_d = CoreService()
-        service_d.name = "d"
-        service_a.dependencies = (service_b.name,)
-        service_c.dependencies = (service_d.name, service_b.name)
-        services = [service_a, service_b, service_c, service_d]
-        expected = {service_a.name, service_b.name, service_c.name, service_d.name}
-
-        # when
-        permutations = itertools.permutations(services)
-        for permutation in permutations:
-            permutation = list(permutation)
-            results = ServiceDependencies(permutation).boot_order()
-
-            # then
-            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
diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py
index 6c2e45a6f..c1787b77f 100644
--- a/daemon/tests/test_xml.py
+++ b/daemon/tests/test_xml.py
@@ -4,6 +4,7 @@
 
 import pytest
 
+from core.configservices.utilservices.services import DefaultRouteService
 from core.emulator.data import IpPrefixes, LinkOptions
 from core.emulator.enumerations import EventTypes
 from core.emulator.session import Session
@@ -11,7 +12,6 @@
 from core.location.mobility import BasicRangeModel
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode, WlanNode
-from core.services.utility import SshService
 
 
 class TestXml:
@@ -125,12 +125,10 @@ def test_xml_ptp_services(
         session.add_link(node1.id, node2.id, iface1_data, iface2_data)
 
         # set custom values for node service
-        session.services.set_service(node1.id, SshService.name)
-        service_file = SshService.configs[0]
+        service = node1.config_services[DefaultRouteService.name]
+        file_name = DefaultRouteService.files[0]
         file_data = "# test"
-        session.services.set_service_file(
-            node1.id, SshService.name, service_file, file_data
-        )
+        service.set_template(file_name, file_data)
 
         # instantiate session
         session.instantiate()
@@ -157,12 +155,14 @@ def test_xml_ptp_services(
         session.open_xml(file_path, start=True)
 
         # retrieve custom service
-        service = session.services.get_service(node1.id, SshService.name)
+        node1_xml = session.get_node(node1.id, CoreNode)
+        service_xml = node1_xml.config_services[DefaultRouteService.name]
 
         # verify nodes have been recreated
         assert session.get_node(node1.id, CoreNode)
         assert session.get_node(node2.id, CoreNode)
-        assert service.config_data.get(service_file) == file_data
+        templates = service_xml.get_templates()
+        assert file_data == templates[file_name]
 
     def test_xml_mobility(
         self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes

From fdda30aae780694e3bf6f1e5c425ba79d903f3b7 Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Mon, 25 Sep 2023 16:23:43 -0700
Subject: [PATCH 2/7] consolidated service protobuf messages into one file

---
 daemon/core/api/grpc/client.py                |  2 +-
 daemon/core/api/grpc/grpcutils.py             |  4 +--
 daemon/core/api/grpc/server.py                |  8 ++----
 daemon/core/api/grpc/wrappers.py              | 12 ++------
 .../proto/core/api/grpc/configservices.proto  | 25 +++++++++++++++++
 daemon/proto/core/api/grpc/core.proto         |  5 ++--
 daemon/proto/core/api/grpc/services.proto     | 28 -------------------
 7 files changed, 36 insertions(+), 48 deletions(-)
 delete mode 100644 daemon/proto/core/api/grpc/services.proto

diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index 666230db4..4376dbac5 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -17,6 +17,7 @@
     GetConfigServiceDefaultsRequest,
     GetConfigServiceRenderedRequest,
     GetNodeConfigServiceRequest,
+    ServiceActionRequest,
 )
 from core.api.grpc.core_pb2 import (
     ExecuteScriptRequest,
@@ -38,7 +39,6 @@
     MobilityConfig,
     SetMobilityConfigRequest,
 )
-from core.api.grpc.services_pb2 import ServiceActionRequest
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
     SetWlanConfigRequest,
diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py
index a2d779168..b12ddecc6 100644
--- a/daemon/core/api/grpc/grpcutils.py
+++ b/daemon/core/api/grpc/grpcutils.py
@@ -9,7 +9,7 @@
 from grpc import ServicerContext
 
 from core import utils
-from core.api.grpc import common_pb2, core_pb2, services_pb2, wrappers
+from core.api.grpc import common_pb2, configservices_pb2, core_pb2, wrappers
 from core.api.grpc.configservices_pb2 import ConfigServiceConfig
 from core.api.grpc.emane_pb2 import NodeEmaneConfig
 from core.config import ConfigurableOptions
@@ -759,7 +759,7 @@ def convert_session(session: Session) -> wrappers.Session:
     ]
     default_services = []
     for group, services in session.service_manager.defaults.items():
-        defaults = services_pb2.ServiceDefaults(model=group, services=services)
+        defaults = configservices_pb2.ServiceDefaults(model=group, services=services)
         default_services.append(defaults)
     return core_pb2.Session(
         id=session.id,
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index 8bb079210..60bc67278 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -29,6 +29,9 @@
     GetConfigServiceRenderedResponse,
     GetNodeConfigServiceRequest,
     GetNodeConfigServiceResponse,
+    ServiceAction,
+    ServiceActionRequest,
+    ServiceActionResponse,
 )
 from core.api.grpc.core_pb2 import (
     ExecuteScriptResponse,
@@ -66,11 +69,6 @@
     SetMobilityConfigRequest,
     SetMobilityConfigResponse,
 )
-from core.api.grpc.services_pb2 import (
-    ServiceAction,
-    ServiceActionRequest,
-    ServiceActionResponse,
-)
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
     GetWlanConfigResponse,
diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py
index cab6cc32b..159413e08 100644
--- a/daemon/core/api/grpc/wrappers.py
+++ b/daemon/core/api/grpc/wrappers.py
@@ -5,13 +5,7 @@
 
 from google.protobuf.internal.containers import MessageMap
 
-from core.api.grpc import (
-    common_pb2,
-    configservices_pb2,
-    core_pb2,
-    emane_pb2,
-    services_pb2,
-)
+from core.api.grpc import common_pb2, configservices_pb2, core_pb2, emane_pb2
 
 
 class ConfigServiceValidationMode(Enum):
@@ -205,7 +199,7 @@ class ServiceDefault:
     services: list[str]
 
     @classmethod
-    def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
+    def from_proto(cls, proto: configservices_pb2.ServiceDefaults) -> "ServiceDefault":
         return ServiceDefault(model=proto.model, services=list(proto.services))
 
 
@@ -783,7 +777,7 @@ def to_proto(self) -> core_pb2.Session:
         servers = [x.to_proto() for x in self.servers]
         default_services = []
         for model, services in self.default_services.items():
-            default_service = services_pb2.ServiceDefaults(
+            default_service = configservices_pb2.ServiceDefaults(
                 model=model, services=services
             )
             default_services.append(default_service)
diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto
index 25be616d7..b5e3a9d96 100644
--- a/daemon/proto/core/api/grpc/configservices.proto
+++ b/daemon/proto/core/api/grpc/configservices.proto
@@ -4,6 +4,31 @@ package configservices;
 
 import "core/api/grpc/common.proto";
 
+message ServiceAction {
+    enum Enum {
+        START = 0;
+        STOP = 1;
+        RESTART = 2;
+        VALIDATE = 3;
+    }
+}
+
+message ServiceDefaults {
+    string model = 1;
+    repeated string services = 2;
+}
+
+message ServiceActionRequest {
+    int32 session_id = 1;
+    int32 node_id = 2;
+    string service = 3;
+    ServiceAction.Enum action = 4;
+}
+
+message ServiceActionResponse {
+    bool result = 1;
+}
+
 message ConfigServiceConfig {
     int32 node_id = 1;
     string name = 2;
diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto
index e6f48def0..402aa82d6 100644
--- a/daemon/proto/core/api/grpc/core.proto
+++ b/daemon/proto/core/api/grpc/core.proto
@@ -6,7 +6,6 @@ import "core/api/grpc/configservices.proto";
 import "core/api/grpc/common.proto";
 import "core/api/grpc/emane.proto";
 import "core/api/grpc/mobility.proto";
-import "core/api/grpc/services.proto";
 import "core/api/grpc/wlan.proto";
 
 service CoreApi {
@@ -77,7 +76,7 @@ service CoreApi {
     }
     rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) {
     }
-    rpc ConfigServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) {
+    rpc ConfigServiceAction (configservices.ServiceActionRequest) returns (configservices.ServiceActionResponse) {
     }
     rpc GetConfigServiceRendered (configservices.GetConfigServiceRenderedRequest) returns (configservices.GetConfigServiceRenderedResponse) {
     }
@@ -549,7 +548,7 @@ message Session {
     repeated Link links = 4;
     string dir = 5;
     string user = 6;
-    repeated services.ServiceDefaults default_services = 7;
+    repeated configservices.ServiceDefaults default_services = 7;
     SessionLocation location = 8;
     repeated Hook hooks = 9;
     map<string, string> metadata = 10;
diff --git a/daemon/proto/core/api/grpc/services.proto b/daemon/proto/core/api/grpc/services.proto
deleted file mode 100644
index c88efe2ac..000000000
--- a/daemon/proto/core/api/grpc/services.proto
+++ /dev/null
@@ -1,28 +0,0 @@
-syntax = "proto3";
-
-package services;
-
-message ServiceAction {
-    enum Enum {
-        START = 0;
-        STOP = 1;
-        RESTART = 2;
-        VALIDATE = 3;
-    }
-}
-
-message ServiceDefaults {
-    string model = 1;
-    repeated string services = 2;
-}
-
-message ServiceActionRequest {
-    int32 session_id = 1;
-    int32 node_id = 2;
-    string service = 3;
-    ServiceAction.Enum action = 4;
-}
-
-message ServiceActionResponse {
-    bool result = 1;
-}

From c7dcb91dfdc8232a9f82fbb78edc5b64efe5488d Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Tue, 26 Sep 2023 09:32:10 -0700
Subject: [PATCH 3/7] grpc: adjusting config services within grpc/proto to now
 be just services

---
 daemon/core/api/grpc/client.py                | 26 ++++----
 daemon/core/api/grpc/grpcutils.py             | 17 +++--
 daemon/core/api/grpc/server.py                | 62 +++++++++----------
 daemon/core/api/grpc/wrappers.py              | 26 ++++----
 daemon/core/gui/coreclient.py                 | 21 +------
 daemon/proto/core/api/grpc/core.proto         | 20 +++---
 .../{configservices.proto => services.proto}  | 28 ++++-----
 7 files changed, 85 insertions(+), 115 deletions(-)
 rename daemon/proto/core/api/grpc/{configservices.proto => services.proto} (72%)

diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index 4376dbac5..ef8a91ca3 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -13,12 +13,6 @@
 import grpc
 
 from core.api.grpc import core_pb2, core_pb2_grpc, emane_pb2, wrappers
-from core.api.grpc.configservices_pb2 import (
-    GetConfigServiceDefaultsRequest,
-    GetConfigServiceRenderedRequest,
-    GetNodeConfigServiceRequest,
-    ServiceActionRequest,
-)
 from core.api.grpc.core_pb2 import (
     ExecuteScriptRequest,
     GetConfigRequest,
@@ -39,6 +33,12 @@
     MobilityConfig,
     SetMobilityConfigRequest,
 )
+from core.api.grpc.services_pb2 import (
+    GetNodeServiceRequest,
+    GetServiceDefaultsRequest,
+    GetServiceRenderedRequest,
+    ServiceActionRequest,
+)
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
     SetWlanConfigRequest,
@@ -739,7 +739,7 @@ def config_service_action(
         request = ServiceActionRequest(
             session_id=session_id, node_id=node_id, service=service, action=action.value
         )
-        response = self.stub.ConfigServiceAction(request)
+        response = self.stub.ServiceAction(request)
         return response.result
 
     def get_wlan_config(
@@ -877,10 +877,10 @@ def get_config_service_defaults(
         :param name: name of service to get defaults for
         :return: config service defaults
         """
-        request = GetConfigServiceDefaultsRequest(
+        request = GetServiceDefaultsRequest(
             name=name, session_id=session_id, node_id=node_id
         )
-        response = self.stub.GetConfigServiceDefaults(request)
+        response = self.stub.GetServiceDefaults(request)
         return wrappers.ConfigServiceDefaults.from_proto(response)
 
     def get_node_config_service(
@@ -895,10 +895,10 @@ def get_node_config_service(
         :return: config dict of names to values
         :raises grpc.RpcError: when session or node doesn't exist
         """
-        request = GetNodeConfigServiceRequest(
+        request = GetNodeServiceRequest(
             session_id=session_id, node_id=node_id, name=name
         )
-        response = self.stub.GetNodeConfigService(request)
+        response = self.stub.GetNodeService(request)
         return dict(response.config)
 
     def get_config_service_rendered(
@@ -912,10 +912,10 @@ def get_config_service_rendered(
         :param name: name of service
         :return: dict mapping names of files to rendered data
         """
-        request = GetConfigServiceRenderedRequest(
+        request = GetServiceRenderedRequest(
             session_id=session_id, node_id=node_id, name=name
         )
-        response = self.stub.GetConfigServiceRendered(request)
+        response = self.stub.GetServiceRendered(request)
         return dict(response.rendered)
 
     def get_emane_event_channel(
diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py
index b12ddecc6..6b6fa9cef 100644
--- a/daemon/core/api/grpc/grpcutils.py
+++ b/daemon/core/api/grpc/grpcutils.py
@@ -9,8 +9,7 @@
 from grpc import ServicerContext
 
 from core import utils
-from core.api.grpc import common_pb2, configservices_pb2, core_pb2, wrappers
-from core.api.grpc.configservices_pb2 import ConfigServiceConfig
+from core.api.grpc import common_pb2, core_pb2, services_pb2, wrappers
 from core.api.grpc.emane_pb2 import NodeEmaneConfig
 from core.config import ConfigurableOptions
 from core.emane.nodes import EmaneNet, EmaneOptions
@@ -73,7 +72,7 @@ def add_node_data(
     options.canvas = node_proto.canvas
     if isinstance(options, CoreNodeOptions):
         options.model = node_proto.model
-        options.config_services = node_proto.config_services
+        options.config_services = node_proto.services
     if isinstance(options, EmaneOptions):
         options.emane_model = node_proto.emane
     if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
@@ -342,7 +341,7 @@ def get_node_proto(
         for service in node.config_services.values():
             if not service.custom_templates and not service.custom_config:
                 continue
-            config_service_configs[service.name] = ConfigServiceConfig(
+            config_service_configs[service.name] = services_pb2.ServiceConfig(
                 node_id=node.id,
                 name=service.name,
                 templates=service.custom_templates,
@@ -358,14 +357,14 @@ def get_node_proto(
         geo=geo,
         icon=node.icon,
         image=image,
-        config_services=config_services,
+        services=config_services,
         dir=node_dir,
         channel=channel,
         canvas=node.canvas,
         wlan_config=wlan_config,
         wireless_config=wireless_config,
         mobility_config=mobility_config,
-        config_service_configs=config_service_configs,
+        service_configs=config_service_configs,
         emane_configs=emane_configs,
     )
 
@@ -759,7 +758,7 @@ def convert_session(session: Session) -> wrappers.Session:
     ]
     default_services = []
     for group, services in session.service_manager.defaults.items():
-        defaults = configservices_pb2.ServiceDefaults(model=group, services=services)
+        defaults = services_pb2.ServiceDefaults(model=group, services=services)
         default_services.append(defaults)
     return core_pb2.Session(
         id=session.id,
@@ -803,13 +802,13 @@ def configure_node(
     if isinstance(core_node, WirelessNode) and node.wireless_config:
         config = {k: v.value for k, v in node.wireless_config.items()}
         core_node.set_config(config)
-    if node.config_service_configs:
+    if node.service_configs:
         if not isinstance(core_node, CoreNode):
             context.abort(
                 grpc.StatusCode.INVALID_ARGUMENT,
                 "invalid node type with config service configs",
             )
-        for service_name, service_config in node.config_service_configs.items():
+        for service_name, service_config in node.service_configs.items():
             service = core_node.config_services[service_name]
             if service_config.config:
                 service.set_config(dict(service_config.config))
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index 60bc67278..b9babce6b 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -15,24 +15,7 @@
 from grpc import ServicerContext
 
 from core import utils
-from core.api.grpc import (
-    common_pb2,
-    configservices_pb2,
-    core_pb2,
-    core_pb2_grpc,
-    grpcutils,
-)
-from core.api.grpc.configservices_pb2 import (
-    GetConfigServiceDefaultsRequest,
-    GetConfigServiceDefaultsResponse,
-    GetConfigServiceRenderedRequest,
-    GetConfigServiceRenderedResponse,
-    GetNodeConfigServiceRequest,
-    GetNodeConfigServiceResponse,
-    ServiceAction,
-    ServiceActionRequest,
-    ServiceActionResponse,
-)
+from core.api.grpc import common_pb2, core_pb2, core_pb2_grpc, grpcutils, services_pb2
 from core.api.grpc.core_pb2 import (
     ExecuteScriptResponse,
     GetWirelessConfigRequest,
@@ -69,6 +52,17 @@
     SetMobilityConfigRequest,
     SetMobilityConfigResponse,
 )
+from core.api.grpc.services_pb2 import (
+    GetNodeServiceRequest,
+    GetNodeServiceResponse,
+    GetServiceDefaultsRequest,
+    GetServiceDefaultsResponse,
+    GetServiceRenderedRequest,
+    GetServiceRenderedResponse,
+    ServiceAction,
+    ServiceActionRequest,
+    ServiceActionResponse,
+)
 from core.api.grpc.wlan_pb2 import (
     GetWlanConfigRequest,
     GetWlanConfigResponse,
@@ -221,7 +215,7 @@ def GetConfig(
     ) -> core_pb2.GetConfigResponse:
         config_services = []
         for service in self.coreemu.service_manager.services.values():
-            service_proto = configservices_pb2.ConfigService(
+            service_proto = services_pb2.Service(
                 name=service.name,
                 group=service.group,
                 executables=service.executables,
@@ -238,7 +232,7 @@ def GetConfig(
             config_services.append(service_proto)
         emane_models = [x.name for x in EmaneModelManager.models.values()]
         return core_pb2.GetConfigResponse(
-            config_services=config_services,
+            services=config_services,
             emane_models=emane_models,
         )
 
@@ -904,7 +898,7 @@ def MobilityAction(
             result = False
         return MobilityActionResponse(result=result)
 
-    def ConfigServiceAction(
+    def ServiceAction(
         self, request: ServiceActionRequest, context: ServicerContext
     ) -> ServiceActionResponse:
         """
@@ -1104,9 +1098,9 @@ def EmaneLink(
         else:
             return EmaneLinkResponse(result=False)
 
-    def GetNodeConfigService(
-        self, request: GetNodeConfigServiceRequest, context: ServicerContext
-    ) -> GetNodeConfigServiceResponse:
+    def GetNodeService(
+        self, request: GetNodeServiceRequest, context: ServicerContext
+    ) -> GetNodeServiceResponse:
         """
         Gets configuration, for a given configuration service, for a given node.
 
@@ -1123,11 +1117,11 @@ def GetNodeConfigService(
         else:
             service = self.coreemu.service_manager.get_service(request.name)
             config = {x.id: x.default for x in service.default_configs}
-        return GetNodeConfigServiceResponse(config=config)
+        return GetNodeServiceResponse(config=config)
 
-    def GetConfigServiceRendered(
-        self, request: GetConfigServiceRenderedRequest, context: ServicerContext
-    ) -> GetConfigServiceRenderedResponse:
+    def GetServiceRendered(
+        self, request: GetServiceRenderedRequest, context: ServicerContext
+    ) -> GetServiceRenderedResponse:
         """
         Retrieves the rendered file data for a given config service on a node.
 
@@ -1144,11 +1138,11 @@ def GetConfigServiceRendered(
                 grpc.StatusCode.NOT_FOUND, f"unknown node service {request.name}"
             )
         rendered = service.get_rendered_templates()
-        return GetConfigServiceRenderedResponse(rendered=rendered)
+        return GetServiceRenderedResponse(rendered=rendered)
 
-    def GetConfigServiceDefaults(
-        self, request: GetConfigServiceDefaultsRequest, context: ServicerContext
-    ) -> GetConfigServiceDefaultsResponse:
+    def GetServiceDefaults(
+        self, request: GetServiceDefaultsRequest, context: ServicerContext
+    ) -> GetServiceDefaultsResponse:
         """
         Get default values for a given configuration service.
 
@@ -1174,9 +1168,9 @@ def GetConfigServiceDefaults(
             config[configuration.id] = config_option
         modes = []
         for name, mode_config in service.modes.items():
-            mode = configservices_pb2.ConfigMode(name=name, config=mode_config)
+            mode = services_pb2.ConfigMode(name=name, config=mode_config)
             modes.append(mode)
-        return GetConfigServiceDefaultsResponse(
+        return GetServiceDefaultsResponse(
             templates=templates, config=config, modes=modes
         )
 
diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py
index 159413e08..f924568a0 100644
--- a/daemon/core/api/grpc/wrappers.py
+++ b/daemon/core/api/grpc/wrappers.py
@@ -5,7 +5,7 @@
 
 from google.protobuf.internal.containers import MessageMap
 
-from core.api.grpc import common_pb2, configservices_pb2, core_pb2, emane_pb2
+from core.api.grpc import common_pb2, core_pb2, emane_pb2, services_pb2
 
 
 class ConfigServiceValidationMode(Enum):
@@ -121,7 +121,7 @@ class ConfigService:
     validation_period: float
 
     @classmethod
-    def from_proto(cls, proto: configservices_pb2.ConfigService) -> "ConfigService":
+    def from_proto(cls, proto: services_pb2.Service) -> "ConfigService":
         return ConfigService(
             group=proto.group,
             name=proto.name,
@@ -146,9 +146,7 @@ class ConfigServiceConfig:
     config: dict[str, str]
 
     @classmethod
-    def from_proto(
-        cls, proto: configservices_pb2.ConfigServiceConfig
-    ) -> "ConfigServiceConfig":
+    def from_proto(cls, proto: services_pb2.ServiceConfig) -> "ConfigServiceConfig":
         return ConfigServiceConfig(
             node_id=proto.node_id,
             name=proto.name,
@@ -171,7 +169,7 @@ class ConfigServiceDefaults:
 
     @classmethod
     def from_proto(
-        cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse
+        cls, proto: services_pb2.GetServiceDefaultsResponse
     ) -> "ConfigServiceDefaults":
         config = ConfigOption.from_dict(proto.config)
         modes = {x.name: dict(x.config) for x in proto.modes}
@@ -199,7 +197,7 @@ class ServiceDefault:
     services: list[str]
 
     @classmethod
-    def from_proto(cls, proto: configservices_pb2.ServiceDefaults) -> "ServiceDefault":
+    def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault":
         return ServiceDefault(model=proto.model, services=list(proto.services))
 
 
@@ -642,7 +640,7 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             key = (model, iface_id)
             emane_configs[key] = ConfigOption.from_dict(emane_config.config)
         config_service_configs = {}
-        for service, service_config in proto.config_service_configs.items():
+        for service, service_config in proto.service_configs.items():
             config_service_configs[service] = ConfigServiceData(
                 templates=dict(service_config.templates),
                 config=dict(service_config.config),
@@ -653,7 +651,7 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             type=NodeType(proto.type),
             model=proto.model or None,
             position=Position.from_proto(proto.position),
-            config_services=set(proto.config_services),
+            config_services=set(proto.services),
             emane=proto.emane,
             icon=proto.icon,
             image=proto.image,
@@ -682,7 +680,7 @@ def to_proto(self) -> core_pb2.Node:
             emane_configs.append(emane_config)
         config_service_configs = {}
         for service, service_config in self.config_service_configs.items():
-            config_service_configs[service] = configservices_pb2.ConfigServiceConfig(
+            config_service_configs[service] = services_pb2.ServiceConfig(
                 templates=service_config.templates, config=service_config.config
             )
         return core_pb2.Node(
@@ -691,7 +689,7 @@ def to_proto(self) -> core_pb2.Node:
             type=self.type.value,
             model=self.model,
             position=self.position.to_proto(),
-            config_services=self.config_services,
+            services=self.config_services,
             emane=self.emane,
             icon=self.icon,
             image=self.image,
@@ -701,7 +699,7 @@ def to_proto(self) -> core_pb2.Node:
             canvas=self.canvas,
             wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()},
             mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()},
-            config_service_configs=config_service_configs,
+            service_configs=config_service_configs,
             emane_configs=emane_configs,
             wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
         )
@@ -777,7 +775,7 @@ def to_proto(self) -> core_pb2.Session:
         servers = [x.to_proto() for x in self.servers]
         default_services = []
         for model, services in self.default_services.items():
-            default_service = configservices_pb2.ServiceDefaults(
+            default_service = services_pb2.ServiceDefaults(
                 model=model, services=services
             )
             default_services.append(default_service)
@@ -857,7 +855,7 @@ class CoreConfig:
 
     @classmethod
     def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
-        config_services = [ConfigService.from_proto(x) for x in proto.config_services]
+        config_services = [ConfigService.from_proto(x) for x in proto.services]
         return CoreConfig(
             config_services=config_services,
             emane_models=list(proto.emane_models),
diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py
index e95aff08a..1f9cef375 100644
--- a/daemon/core/gui/coreclient.py
+++ b/daemon/core/gui/coreclient.py
@@ -13,7 +13,7 @@
 
 import grpc
 
-from core.api.grpc import client, configservices_pb2, core_pb2
+from core.api.grpc import client, core_pb2
 from core.api.grpc.wrappers import (
     ConfigOption,
     ConfigService,
@@ -693,25 +693,6 @@ def get_config_service_defaults(
     ) -> ConfigServiceDefaults:
         return self.client.get_config_service_defaults(self.session.id, node_id, name)
 
-    def get_config_service_configs_proto(
-        self,
-    ) -> list[configservices_pb2.ConfigServiceConfig]:
-        config_service_protos = []
-        for node in self.session.nodes.values():
-            if not nutils.is_container(node):
-                continue
-            if not node.config_service_configs:
-                continue
-            for name, service_config in node.config_service_configs.items():
-                config_proto = configservices_pb2.ConfigServiceConfig(
-                    node_id=node.id,
-                    name=name,
-                    templates=service_config.templates,
-                    config=service_config.config,
-                )
-                config_service_protos.append(config_proto)
-        return config_service_protos
-
     def run(self, node_id: int) -> str:
         logger.info("running node(%s) cmd: %s", node_id, self.observer)
         _, output = self.client.node_command(self.session.id, node_id, self.observer)
diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto
index 402aa82d6..9733f7dbe 100644
--- a/daemon/proto/core/api/grpc/core.proto
+++ b/daemon/proto/core/api/grpc/core.proto
@@ -2,7 +2,7 @@ syntax = "proto3";
 
 package core;
 
-import "core/api/grpc/configservices.proto";
+import "core/api/grpc/services.proto";
 import "core/api/grpc/common.proto";
 import "core/api/grpc/emane.proto";
 import "core/api/grpc/mobility.proto";
@@ -71,14 +71,14 @@ service CoreApi {
     rpc MobilityAction (mobility.MobilityActionRequest) returns (mobility.MobilityActionResponse) {
     }
 
-    // config services
-    rpc GetConfigServiceDefaults (configservices.GetConfigServiceDefaultsRequest) returns (configservices.GetConfigServiceDefaultsResponse) {
+    // services
+    rpc GetServiceDefaults (services.GetServiceDefaultsRequest) returns (services.GetServiceDefaultsResponse) {
     }
-    rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) {
+    rpc GetNodeService (services.GetNodeServiceRequest) returns (services.GetNodeServiceResponse) {
     }
-    rpc ConfigServiceAction (configservices.ServiceActionRequest) returns (configservices.ServiceActionResponse) {
+    rpc ServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) {
     }
-    rpc GetConfigServiceRendered (configservices.GetConfigServiceRenderedRequest) returns (configservices.GetConfigServiceRenderedResponse) {
+    rpc GetServiceRendered (services.GetServiceRenderedRequest) returns (services.GetServiceRenderedResponse) {
     }
 
     // wlan rpc
@@ -133,7 +133,7 @@ message GetConfigRequest {
 }
 
 message GetConfigResponse {
-    repeated configservices.ConfigService config_services = 1;
+    repeated services.Service services = 1;
     repeated string emane_models = 2;
 }
 
@@ -548,7 +548,7 @@ message Session {
     repeated Link links = 4;
     string dir = 5;
     string user = 6;
-    repeated configservices.ServiceDefaults default_services = 7;
+    repeated services.ServiceDefaults default_services = 7;
     SessionLocation location = 8;
     repeated Hook hooks = 9;
     map<string, string> metadata = 10;
@@ -575,14 +575,14 @@ message Node {
     string icon = 7;
     string image = 8;
     string server = 9;
-    repeated string config_services = 10;
+    repeated string services = 10;
     Geo geo = 11;
     string dir = 12;
     string channel = 13;
     int32 canvas = 14;
     map<string, common.ConfigOption> wlan_config = 15;
     map<string, common.ConfigOption> mobility_config = 16;
-    map<string, configservices.ConfigServiceConfig> config_service_configs= 17;
+    map<string, services.ServiceConfig> service_configs= 17;
     repeated emane.NodeEmaneConfig emane_configs = 18;
     map<string, common.ConfigOption> wireless_config = 19;
 }
diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/services.proto
similarity index 72%
rename from daemon/proto/core/api/grpc/configservices.proto
rename to daemon/proto/core/api/grpc/services.proto
index b5e3a9d96..bed288d5f 100644
--- a/daemon/proto/core/api/grpc/configservices.proto
+++ b/daemon/proto/core/api/grpc/services.proto
@@ -1,6 +1,6 @@
 syntax = "proto3";
 
-package configservices;
+package services;
 
 import "core/api/grpc/common.proto";
 
@@ -29,14 +29,12 @@ message ServiceActionResponse {
     bool result = 1;
 }
 
-message ConfigServiceConfig {
-    int32 node_id = 1;
-    string name = 2;
-    map<string, string> templates = 3;
-    map<string, string> config = 4;
+message ServiceConfig {
+    map<string, string> templates = 1;
+    map<string, string> config = 2;
 }
 
-message ConfigServiceValidationMode {
+message ServiceValidationMode {
     enum Enum {
         BLOCKING = 0;
         NON_BLOCKING = 1;
@@ -44,7 +42,7 @@ message ConfigServiceValidationMode {
     }
 }
 
-message ConfigService {
+message Service {
     string group = 1;
     string name = 2;
     repeated string executables = 3;
@@ -54,7 +52,7 @@ message ConfigService {
     repeated string startup = 7;
     repeated string validate = 8;
     repeated string shutdown = 9;
-    ConfigServiceValidationMode.Enum validation_mode = 10;
+    ServiceValidationMode.Enum validation_mode = 10;
     int32 validation_timer = 11;
     float validation_period = 12;
 }
@@ -64,34 +62,34 @@ message ConfigMode {
     map<string, string> config = 2;
 }
 
-message GetConfigServiceDefaultsRequest {
+message GetServiceDefaultsRequest {
     string name = 1;
     int32 session_id = 2;
     int32 node_id = 3;
 }
 
-message GetConfigServiceDefaultsResponse {
+message GetServiceDefaultsResponse {
     map<string, string> templates = 1;
     map<string, common.ConfigOption> config = 2;
     repeated ConfigMode modes = 3;
 }
 
-message GetNodeConfigServiceRequest {
+message GetNodeServiceRequest {
     int32 session_id = 1;
     int32 node_id = 2;
     string name = 3;
 }
 
-message GetNodeConfigServiceResponse {
+message GetNodeServiceResponse {
     map<string, string> config = 1;
 }
 
-message GetConfigServiceRenderedRequest {
+message GetServiceRenderedRequest {
     int32 session_id = 1;
     int32 node_id = 2;
     string name = 3;
 }
 
-message GetConfigServiceRenderedResponse {
+message GetServiceRenderedResponse {
     map<string, string> rendered = 1;
 }

From 11526b4ca767ee8fefe20af4751f4185edd88228 Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Tue, 26 Sep 2023 09:44:33 -0700
Subject: [PATCH 4/7] grpc: updated wrapper classes to reflect config service
 to just service naming

---
 daemon/core/api/grpc/client.py                |  4 +-
 daemon/core/api/grpc/wrappers.py              | 62 ++++++++-----------
 daemon/core/gui/coreclient.py                 | 16 +++--
 .../core/gui/dialogs/configserviceconfig.py   | 16 +++--
 daemon/core/gui/dialogs/nodeconfigservice.py  | 14 ++---
 daemon/core/gui/graph/graph.py                |  6 +-
 daemon/core/gui/graph/node.py                 |  2 +-
 daemon/tests/test_grpc.py                     |  6 +-
 .../tutorials/tutorial1/scenario_service.py   |  2 +-
 .../tutorials/tutorial7/scenario_service.py   |  2 +-
 10 files changed, 57 insertions(+), 73 deletions(-)

diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index ef8a91ca3..575501f8e 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -868,7 +868,7 @@ def get_ifaces(self) -> list[str]:
 
     def get_config_service_defaults(
         self, session_id: int, node_id: int, name: str
-    ) -> wrappers.ConfigServiceDefaults:
+    ) -> wrappers.ServiceDefaults:
         """
         Retrieves config service default values.
 
@@ -881,7 +881,7 @@ def get_config_service_defaults(
             name=name, session_id=session_id, node_id=node_id
         )
         response = self.stub.GetServiceDefaults(request)
-        return wrappers.ConfigServiceDefaults.from_proto(response)
+        return wrappers.ServiceDefaults.from_proto(response)
 
     def get_node_config_service(
         self, session_id: int, node_id: int, name: str
diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py
index f924568a0..179a2ba24 100644
--- a/daemon/core/api/grpc/wrappers.py
+++ b/daemon/core/api/grpc/wrappers.py
@@ -8,12 +8,6 @@
 from core.api.grpc import common_pb2, core_pb2, emane_pb2, services_pb2
 
 
-class ConfigServiceValidationMode(Enum):
-    BLOCKING = 0
-    NON_BLOCKING = 1
-    TIMER = 2
-
-
 class ServiceValidationMode(Enum):
     BLOCKING = 0
     NON_BLOCKING = 1
@@ -106,7 +100,7 @@ class EventType:
 
 
 @dataclass
-class ConfigService:
+class Service:
     group: str
     name: str
     executables: list[str]
@@ -116,13 +110,13 @@ class ConfigService:
     startup: list[str]
     validate: list[str]
     shutdown: list[str]
-    validation_mode: ConfigServiceValidationMode
+    validation_mode: ServiceValidationMode
     validation_timer: int
     validation_period: float
 
     @classmethod
-    def from_proto(cls, proto: services_pb2.Service) -> "ConfigService":
-        return ConfigService(
+    def from_proto(cls, proto: services_pb2.Service) -> "Service":
+        return Service(
             group=proto.group,
             name=proto.name,
             executables=list(proto.executables),
@@ -132,37 +126,35 @@ def from_proto(cls, proto: services_pb2.Service) -> "ConfigService":
             startup=list(proto.startup),
             validate=list(proto.validate),
             shutdown=list(proto.shutdown),
-            validation_mode=ConfigServiceValidationMode(proto.validation_mode),
+            validation_mode=ServiceValidationMode(proto.validation_mode),
             validation_timer=proto.validation_timer,
             validation_period=proto.validation_period,
         )
 
 
 @dataclass
-class ConfigServiceConfig:
+class ServiceConfig:
     node_id: int
     name: str
     templates: dict[str, str]
     config: dict[str, str]
 
     @classmethod
-    def from_proto(cls, proto: services_pb2.ServiceConfig) -> "ConfigServiceConfig":
-        return ConfigServiceConfig(
-            node_id=proto.node_id,
-            name=proto.name,
+    def from_proto(cls, proto: services_pb2.ServiceConfig) -> "ServiceConfig":
+        return ServiceConfig(
             templates=dict(proto.templates),
             config=dict(proto.config),
         )
 
 
 @dataclass
-class ConfigServiceData:
+class ServiceData:
     templates: dict[str, str] = field(default_factory=dict)
     config: dict[str, str] = field(default_factory=dict)
 
 
 @dataclass
-class ConfigServiceDefaults:
+class ServiceDefaults:
     templates: dict[str, str]
     config: dict[str, "ConfigOption"]
     modes: dict[str, dict[str, str]]
@@ -170,10 +162,10 @@ class ConfigServiceDefaults:
     @classmethod
     def from_proto(
         cls, proto: services_pb2.GetServiceDefaultsResponse
-    ) -> "ConfigServiceDefaults":
+    ) -> "ServiceDefaults":
         config = ConfigOption.from_dict(proto.config)
         modes = {x.name: dict(x.config) for x in proto.modes}
-        return ConfigServiceDefaults(
+        return ServiceDefaults(
             templates=dict(proto.templates), config=config, modes=modes
         )
 
@@ -610,7 +602,7 @@ class Node:
     type: NodeType = NodeType.DEFAULT
     model: str = None
     position: Position = Position(x=0, y=0)
-    config_services: set[str] = field(default_factory=set)
+    services: set[str] = field(default_factory=set)
     emane: str = None
     icon: str = None
     image: str = None
@@ -627,9 +619,7 @@ class Node:
     wlan_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
     wireless_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
     mobility_config: dict[str, ConfigOption] = field(default_factory=dict, repr=False)
-    config_service_configs: dict[str, ConfigServiceData] = field(
-        default_factory=dict, repr=False
-    )
+    service_configs: dict[str, ServiceData] = field(default_factory=dict, repr=False)
 
     @classmethod
     def from_proto(cls, proto: core_pb2.Node) -> "Node":
@@ -639,9 +629,9 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             model = emane_config.model
             key = (model, iface_id)
             emane_configs[key] = ConfigOption.from_dict(emane_config.config)
-        config_service_configs = {}
+        service_configs = {}
         for service, service_config in proto.service_configs.items():
-            config_service_configs[service] = ConfigServiceData(
+            service_configs[service] = ServiceData(
                 templates=dict(service_config.templates),
                 config=dict(service_config.config),
             )
@@ -651,7 +641,7 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             type=NodeType(proto.type),
             model=proto.model or None,
             position=Position.from_proto(proto.position),
-            config_services=set(proto.services),
+            services=set(proto.services),
             emane=proto.emane,
             icon=proto.icon,
             image=proto.image,
@@ -662,7 +652,7 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node":
             canvas=proto.canvas,
             wlan_config=ConfigOption.from_dict(proto.wlan_config),
             mobility_config=ConfigOption.from_dict(proto.mobility_config),
-            config_service_configs=config_service_configs,
+            service_configs=service_configs,
             emane_model_configs=emane_configs,
             wireless_config=ConfigOption.from_dict(proto.wireless_config),
         )
@@ -678,9 +668,9 @@ def to_proto(self) -> core_pb2.Node:
                 iface_id=iface_id, model=model, config=config
             )
             emane_configs.append(emane_config)
-        config_service_configs = {}
-        for service, service_config in self.config_service_configs.items():
-            config_service_configs[service] = services_pb2.ServiceConfig(
+        service_configs = {}
+        for service, service_config in self.service_configs.items():
+            service_configs[service] = services_pb2.ServiceConfig(
                 templates=service_config.templates, config=service_config.config
             )
         return core_pb2.Node(
@@ -689,7 +679,7 @@ def to_proto(self) -> core_pb2.Node:
             type=self.type.value,
             model=self.model,
             position=self.position.to_proto(),
-            services=self.config_services,
+            services=self.services,
             emane=self.emane,
             icon=self.icon,
             image=self.image,
@@ -699,7 +689,7 @@ def to_proto(self) -> core_pb2.Node:
             canvas=self.canvas,
             wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()},
             mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()},
-            service_configs=config_service_configs,
+            service_configs=service_configs,
             emane_configs=emane_configs,
             wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()},
         )
@@ -850,14 +840,14 @@ def set_options(self, config: dict[str, str]) -> None:
 
 @dataclass
 class CoreConfig:
-    config_services: list[ConfigService] = field(default_factory=list)
+    services: list[Service] = field(default_factory=list)
     emane_models: list[str] = field(default_factory=list)
 
     @classmethod
     def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig":
-        config_services = [ConfigService.from_proto(x) for x in proto.services]
+        services = [Service.from_proto(x) for x in proto.services]
         return CoreConfig(
-            config_services=config_services,
+            services=services,
             emane_models=list(proto.emane_models),
         )
 
diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py
index 1f9cef375..20c6123e8 100644
--- a/daemon/core/gui/coreclient.py
+++ b/daemon/core/gui/coreclient.py
@@ -16,8 +16,6 @@
 from core.api.grpc import client, core_pb2
 from core.api.grpc.wrappers import (
     ConfigOption,
-    ConfigService,
-    ConfigServiceDefaults,
     EmaneModelConfig,
     Event,
     ExceptionEvent,
@@ -30,6 +28,8 @@
     NodeType,
     Position,
     Server,
+    Service,
+    ServiceDefaults,
     Session,
     SessionLocation,
     SessionState,
@@ -74,7 +74,7 @@ def __init__(self, app: "Application", proxy: bool) -> None:
 
         # global service settings
         self.config_services_groups: dict[str, set[str]] = {}
-        self.config_services: dict[str, ConfigService] = {}
+        self.config_services: dict[str, Service] = {}
 
         # loaded configuration data
         self.emane_models: list[str] = []
@@ -355,7 +355,7 @@ def setup(self, session_id: int = None) -> None:
             # get current core configurations services/config services
             core_config = self.client.get_config()
             self.emane_models = sorted(core_config.emane_models)
-            for service in core_config.config_services:
+            for service in core_config.services:
                 self.config_services[service.name] = service
                 group_services = self.config_services_groups.setdefault(
                     service.group, set()
@@ -605,12 +605,12 @@ def create_node(
         )
         if nutils.is_custom(node):
             services = nutils.get_custom_services(self.app.guiconfig, model)
-            node.config_services = set(services)
+            node.services = set(services)
         # assign default services to CORE node
         else:
             services = self.session.default_services.get(model)
             if services:
-                node.config_services = set(services)
+                node.services = set(services)
         logger.info(
             "add node(%s) to session(%s), coordinates(%s, %s)",
             node.name,
@@ -688,9 +688,7 @@ def get_emane_model_configs(self) -> list[EmaneModelConfig]:
     def get_config_service_rendered(self, node_id: int, name: str) -> dict[str, str]:
         return self.client.get_config_service_rendered(self.session.id, node_id, name)
 
-    def get_config_service_defaults(
-        self, node_id: int, name: str
-    ) -> ConfigServiceDefaults:
+    def get_config_service_defaults(self, node_id: int, name: str) -> ServiceDefaults:
         return self.client.get_config_service_defaults(self.session.id, node_id, name)
 
     def run(self, node_id: int) -> str:
diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py
index b78eabba7..a3d19e578 100644
--- a/daemon/core/gui/dialogs/configserviceconfig.py
+++ b/daemon/core/gui/dialogs/configserviceconfig.py
@@ -10,8 +10,8 @@
 
 from core.api.grpc.wrappers import (
     ConfigOption,
-    ConfigServiceData,
     Node,
+    ServiceData,
     ServiceValidationMode,
 )
 from core.gui.dialogs.dialog import Dialog
@@ -99,7 +99,7 @@ def load(self) -> None:
             self.rendered = self.core.get_config_service_rendered(
                 self.node.id, self.service_name
             )
-            service_config = self.node.config_service_configs.get(self.service_name)
+            service_config = self.node.service_configs.get(self.service_name)
             if service_config:
                 for key, value in service_config.config.items():
                     self.config[key].value = value
@@ -329,12 +329,12 @@ def draw_buttons(self) -> None:
     def click_apply(self) -> None:
         current_listbox = self.master.current.listbox
         if not self.is_custom():
-            self.node.config_service_configs.pop(self.service_name, None)
+            self.node.service_configs.pop(self.service_name, None)
             current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
             self.destroy()
             return
-        service_config = self.node.config_service_configs.setdefault(
-            self.service_name, ConfigServiceData()
+        service_config = self.node.service_configs.setdefault(
+            self.service_name, ServiceData()
         )
         if self.config_frame:
             self.config_frame.parse_config()
@@ -381,16 +381,14 @@ def is_custom(self) -> bool:
     def click_defaults(self) -> None:
         # clear all saved state data
         self.modified_files.clear()
-        self.node.config_service_configs.pop(self.service_name, None)
+        self.node.service_configs.pop(self.service_name, None)
         self.temp_service_files = dict(self.original_service_files)
         # reset session definition and retrieve default rendered templates
         self.core.start_session(definition=True)
         self.rendered = self.core.get_config_service_rendered(
             self.node.id, self.service_name
         )
-        logger.info(
-            "cleared config service config: %s", self.node.config_service_configs
-        )
+        logger.info("cleared config service config: %s", self.node.service_configs)
         # reset current selected file data and config data, if present
         template_name = self.templates_combobox.get()
         temp_data = self.temp_service_files[template_name]
diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py
index ce718080f..44b1c4359 100644
--- a/daemon/core/gui/dialogs/nodeconfigservice.py
+++ b/daemon/core/gui/dialogs/nodeconfigservice.py
@@ -29,7 +29,7 @@ def __init__(
         self.services: Optional[CheckboxList] = None
         self.current: Optional[ListboxScroll] = None
         if services is None:
-            services = set(node.config_services)
+            services = set(node.services)
         self.current_services: set[str] = services
         self.protocol("WM_DELETE_WINDOW", self.click_cancel)
         self.draw()
@@ -103,9 +103,9 @@ def service_clicked(self, name: str, var: tk.IntVar) -> None:
             self.current_services.add(name)
         elif not var.get() and name in self.current_services:
             self.current_services.remove(name)
-            self.node.config_service_configs.pop(name, None)
+            self.node.service_configs.pop(name, None)
         self.draw_current_services()
-        self.node.config_services = self.current_services.copy()
+        self.node.services = self.current_services.copy()
 
     def click_configure(self) -> None:
         current_selection = self.current.listbox.curselection()
@@ -134,8 +134,8 @@ def draw_current_services(self) -> None:
                 self.current.listbox.itemconfig(tk.END, bg="green")
 
     def click_save(self) -> None:
-        self.node.config_services = self.current_services.copy()
-        logger.info("saved node config services: %s", self.node.config_services)
+        self.node.services = self.current_services.copy()
+        logger.info("saved node config services: %s", self.node.services)
         self.destroy()
 
     def click_cancel(self) -> None:
@@ -148,11 +148,11 @@ def click_remove(self) -> None:
             service = self.current.listbox.get(cur[0])
             self.current.listbox.delete(cur[0])
             self.current_services.remove(service)
-            self.node.config_service_configs.pop(service, None)
+            self.node.service_configs.pop(service, None)
             for checkbutton in self.services.frame.winfo_children():
                 if checkbutton["text"] == service:
                     checkbutton.invoke()
                     return
 
     def is_custom_service(self, service: str) -> bool:
-        return service in self.node.config_service_configs
+        return service in self.node.service_configs
diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py
index d2e8b5db5..7ac741551 100644
--- a/daemon/core/gui/graph/graph.py
+++ b/daemon/core/gui/graph/graph.py
@@ -720,7 +720,7 @@ def paste_selected(self, _event: tk.Event = None) -> None:
             )
             # copy configurations and services
             node.core_node.services = core_node.services.copy()
-            node.core_node.config_services = core_node.config_services.copy()
+            node.core_node.services = core_node.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)
@@ -728,9 +728,7 @@ def paste_selected(self, _event: tk.Event = None) -> None:
             node.core_node.service_file_configs = deepcopy(
                 core_node.service_file_configs
             )
-            node.core_node.config_service_configs = deepcopy(
-                core_node.config_service_configs
-            )
+            node.core_node.service_configs = deepcopy(core_node.service_configs)
             node.core_node.image = core_node.image
 
             copy_map[canvas_node.id] = node.id
diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py
index b63f4bf5b..9437800d4 100644
--- a/daemon/core/gui/graph/node.py
+++ b/daemon/core/gui/graph/node.py
@@ -242,7 +242,7 @@ def show_context(self, event: tk.Event) -> None:
                 )
             if nutils.is_container(self.core_node):
                 services_menu = tk.Menu(self.context)
-                for service in sorted(self.core_node.config_services):
+                for service in sorted(self.core_node.services):
                     service_menu = tk.Menu(services_menu)
                     themes.style_menu(service_menu)
                     start_func = functools.partial(self.start_service, service)
diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py
index ded03d317..cee7fae16 100644
--- a/daemon/tests/test_grpc.py
+++ b/daemon/tests/test_grpc.py
@@ -14,7 +14,6 @@
 from core.api.grpc.wrappers import (
     ConfigOption,
     ConfigOptionType,
-    ConfigServiceData,
     EmaneModelConfig,
     Event,
     Geo,
@@ -28,6 +27,7 @@
     NodeType,
     Position,
     ServiceAction,
+    ServiceData,
     SessionLocation,
     SessionState,
 )
@@ -96,10 +96,10 @@ def test_start_session(self, grpc_server: CoreGrpcServer, definition):
         service_name = DefaultRouteService.name
         file_name = DefaultRouteService.files[0]
         file_data = "hello world"
-        service_data = ConfigServiceData(
+        service_data = ServiceData(
             templates={file_name: file_data},
         )
-        node1.config_service_configs[service_name] = service_data
+        node1.service_configs[service_name] = service_data
 
         # setup session option
         option_key = "controlnet"
diff --git a/package/share/tutorials/tutorial1/scenario_service.py b/package/share/tutorials/tutorial1/scenario_service.py
index 5a3c55088..d0de570f6 100644
--- a/package/share/tutorials/tutorial1/scenario_service.py
+++ b/package/share/tutorials/tutorial1/scenario_service.py
@@ -19,7 +19,7 @@ def main():
     # create nodes
     position = Position(x=250, y=250)
     node1 = session.add_node(_id=1, name="n1", position=position)
-    node1.config_services.add("ChatApp Server")
+    node1.services.add("ChatApp Server")
     position = Position(x=500, y=250)
     node2 = session.add_node(_id=2, name="n2", position=position)
 
diff --git a/package/share/tutorials/tutorial7/scenario_service.py b/package/share/tutorials/tutorial7/scenario_service.py
index f0626ac2e..1c92e83c6 100644
--- a/package/share/tutorials/tutorial7/scenario_service.py
+++ b/package/share/tutorials/tutorial7/scenario_service.py
@@ -28,7 +28,7 @@ def main():
     )
     position = Position(x=250, y=250)
     node2 = session.add_node(_id=2, model="mdr", name="n2", position=position)
-    node2.config_services.add("ChatApp Server")
+    node2.services.add("ChatApp Server")
     position = Position(x=500, y=250)
     node3 = session.add_node(_id=3, model="mdr", name="n3", position=position)
 

From b0a6a7370f0a1c3e97b966747eb5da0647937aea Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Tue, 26 Sep 2023 10:58:58 -0700
Subject: [PATCH 5/7] daemon: updates to python package structure to reflect
 config services now being just services

---
 daemon/core/api/grpc/server.py                | 12 ++---
 daemon/core/emulator/coreemu.py               |  4 +-
 daemon/core/emulator/session.py               |  4 +-
 daemon/core/nodes/base.py                     | 10 ++--
 .../{configservice => services}/__init__.py   |  0
 .../core/{configservice => services}/base.py  | 36 ++++++-------
 .../defaults}/__init__.py                     |  0
 .../defaults}/frrservices/__init__.py         |  0
 .../defaults}/frrservices/services.py         | 22 ++++----
 .../frrservices/templates/frrboot.sh          |  0
 .../templates/usr/local/etc/frr/daemons       |  0
 .../templates/usr/local/etc/frr/frr.conf      |  0
 .../templates/usr/local/etc/frr/vtysh.conf    |  0
 .../defaults}/nrlservices/__init__.py         |  0
 .../defaults}/nrlservices/services.py         | 30 +++++------
 .../templates/etc/olsrd/olsrd.conf            |  0
 .../nrlservices/templates/mgensink.sh         |  0
 .../nrlservices/templates/nrlnhdp.sh          |  0
 .../nrlservices/templates/nrlolsrd.sh         |  0
 .../nrlservices/templates/nrlolsrv2.sh        |  0
 .../defaults}/nrlservices/templates/olsrd.sh  |  0
 .../defaults}/nrlservices/templates/sink.mgen |  0
 .../nrlservices/templates/start_mgen_actor.sh |  0
 .../nrlservices/templates/startsmf.sh         |  0
 .../defaults}/quaggaservices/__init__.py      |  0
 .../defaults}/quaggaservices/services.py      | 22 ++++----
 .../quaggaservices/templates/quaggaboot.sh    |  0
 .../usr/local/etc/quagga/Quagga.conf          |  0
 .../templates/usr/local/etc/quagga/vtysh.conf |  0
 .../defaults}/securityservices/__init__.py    |  0
 .../defaults}/securityservices/services.py    | 22 ++++----
 .../securityservices/templates/firewall.sh    |  0
 .../securityservices/templates/ipsec.sh       |  0
 .../securityservices/templates/nat.sh         |  0
 .../securityservices/templates/vpnclient.sh   |  0
 .../securityservices/templates/vpnserver.sh   |  0
 .../defaults}/utilservices/__init__.py        |  0
 .../defaults}/utilservices/services.py        | 50 +++++++++----------
 .../utilservices/templates/defaultmroute.sh   |  0
 .../utilservices/templates/defaultroute.sh    |  0
 .../templates/etc/apache2/apache2.conf        |  0
 .../templates/etc/apache2/envvars             |  0
 .../templates/etc/dhcp/dhcpd.conf             |  0
 .../templates/etc/radvd/radvd.conf            |  0
 .../templates/etc/ssh/sshd_config             |  0
 .../utilservices/templates/ipforward.sh       |  0
 .../defaults}/utilservices/templates/pcap.sh  |  0
 .../utilservices/templates/startatd.sh        |  0
 .../utilservices/templates/startdhcpclient.sh |  0
 .../utilservices/templates/startsshd.sh       |  0
 .../utilservices/templates/staticroute.sh     |  0
 .../utilservices/templates/var/www/index.html |  0
 .../utilservices/templates/vsftpd.conf        |  0
 .../dependencies.py                           | 22 ++++----
 .../{configservice => services}/manager.py    | 35 ++++++-------
 daemon/tests/test_grpc.py                     |  2 +-
 ...st_config_services.py => test_services.py} | 30 +++++------
 daemon/tests/test_xml.py                      |  2 +-
 docs/configservices.md                        |  6 +--
 .../tutorials/chatapp/chatapp_service.py      |  6 +--
 60 files changed, 155 insertions(+), 160 deletions(-)
 rename daemon/core/{configservice => services}/__init__.py (100%)
 rename daemon/core/{configservice => services}/base.py (94%)
 rename daemon/core/{configservices => services/defaults}/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/frrservices/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/frrservices/services.py (95%)
 rename daemon/core/{configservices => services/defaults}/frrservices/templates/frrboot.sh (100%)
 rename daemon/core/{configservices => services/defaults}/frrservices/templates/usr/local/etc/frr/daemons (100%)
 rename daemon/core/{configservices => services/defaults}/frrservices/templates/usr/local/etc/frr/frr.conf (100%)
 rename daemon/core/{configservices => services/defaults}/frrservices/templates/usr/local/etc/frr/vtysh.conf (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/services.py (86%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/etc/olsrd/olsrd.conf (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/mgensink.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/nrlnhdp.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/nrlolsrd.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/nrlolsrv2.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/olsrd.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/sink.mgen (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/start_mgen_actor.sh (100%)
 rename daemon/core/{configservices => services/defaults}/nrlservices/templates/startsmf.sh (100%)
 rename daemon/core/{configservices => services/defaults}/quaggaservices/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/quaggaservices/services.py (95%)
 rename daemon/core/{configservices => services/defaults}/quaggaservices/templates/quaggaboot.sh (100%)
 rename daemon/core/{configservices => services/defaults}/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf (100%)
 rename daemon/core/{configservices => services/defaults}/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/services.py (85%)
 rename daemon/core/{configservices => services/defaults}/securityservices/templates/firewall.sh (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/templates/ipsec.sh (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/templates/nat.sh (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/templates/vpnclient.sh (100%)
 rename daemon/core/{configservices => services/defaults}/securityservices/templates/vpnserver.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/__init__.py (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/services.py (86%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/defaultmroute.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/defaultroute.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/etc/apache2/apache2.conf (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/etc/apache2/envvars (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/etc/dhcp/dhcpd.conf (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/etc/radvd/radvd.conf (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/etc/ssh/sshd_config (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/ipforward.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/pcap.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/startatd.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/startdhcpclient.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/startsshd.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/staticroute.sh (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/var/www/index.html (100%)
 rename daemon/core/{configservices => services/defaults}/utilservices/templates/vsftpd.conf (100%)
 rename daemon/core/{configservice => services}/dependencies.py (84%)
 rename daemon/core/{configservice => services}/manager.py (72%)
 rename daemon/tests/{test_config_services.py => test_services.py} (91%)

diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index b9babce6b..be31442b1 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -71,7 +71,6 @@
     WlanLinkRequest,
     WlanLinkResponse,
 )
-from core.configservice.base import ConfigService, ConfigServiceBootError
 from core.emane.modelmanager import EmaneModelManager
 from core.emulator.coreemu import CoreEmu
 from core.emulator.data import InterfaceData, LinkData, LinkOptions
@@ -87,6 +86,7 @@
 from core.nodes.base import CoreNode, NodeBase
 from core.nodes.network import CoreNetwork, WlanNode
 from core.nodes.wireless import WirelessNode
+from core.services.base import Service, ServiceBootError
 from core.xml.corexml import CoreXmlWriter
 
 logger = logging.getLogger(__name__)
@@ -194,9 +194,7 @@ def move_node(
         source = source if source else None
         session.broadcast_node(node, source=source)
 
-    def validate_service(
-        self, name: str, context: ServicerContext
-    ) -> type[ConfigService]:
+    def validate_service(self, name: str, context: ServicerContext) -> type[Service]:
         """
         Validates a configuration service is a valid known service.
 
@@ -920,7 +918,7 @@ def ServiceAction(
             try:
                 service.start()
                 result = True
-            except ConfigServiceBootError:
+            except ServiceBootError:
                 pass
         elif request.action == ServiceAction.STOP:
             service.stop()
@@ -930,13 +928,13 @@ def ServiceAction(
             try:
                 service.start()
                 result = True
-            except ConfigServiceBootError:
+            except ServiceBootError:
                 pass
         elif request.action == ServiceAction.VALIDATE:
             try:
                 service.run_validation()
                 result = True
-            except ConfigServiceBootError:
+            except ServiceBootError:
                 pass
         return ServiceActionResponse(result=result)
 
diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py
index 9680523fe..569f76bc3 100644
--- a/daemon/core/emulator/coreemu.py
+++ b/daemon/core/emulator/coreemu.py
@@ -3,10 +3,10 @@
 from pathlib import Path
 
 from core import utils
-from core.configservice.manager import ConfigServiceManager
 from core.emane.modelmanager import EmaneModelManager
 from core.emulator.session import Session
 from core.executables import get_requirements
+from core.services.manager import ServiceManager
 
 logger = logging.getLogger(__name__)
 
@@ -36,7 +36,7 @@ def __init__(self, config: dict[str, str] = None) -> None:
 
         # load services
         self.service_errors: list[str] = []
-        self.service_manager: ConfigServiceManager = ConfigServiceManager()
+        self.service_manager: ServiceManager = ServiceManager()
         self._load_services()
 
         # check and load emane
diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py
index c0eda3682..43cd8c541 100644
--- a/daemon/core/emulator/session.py
+++ b/daemon/core/emulator/session.py
@@ -17,7 +17,6 @@
 from typing import Callable, Optional, TypeVar, Union
 
 from core import constants, utils
-from core.configservice.manager import ConfigServiceManager
 from core.emane.emanemanager import EmaneManager, EmaneState
 from core.emane.nodes import EmaneNet
 from core.emulator.data import (
@@ -58,6 +57,7 @@
 from core.nodes.podman import PodmanNode
 from core.nodes.wireless import WirelessNode
 from core.plugins.sdt import Sdt
+from core.services.manager import ServiceManager
 from core.xml import corexml, corexmldeployment
 from core.xml.corexml import CoreXmlReader, CoreXmlWriter
 
@@ -155,7 +155,7 @@ def __init__(
         self.sdt: Sdt = Sdt(self)
 
         # config services
-        self.service_manager: Optional[ConfigServiceManager] = None
+        self.service_manager: Optional[ServiceManager] = None
 
     @classmethod
     def get_node_class(cls, _type: NodeTypes) -> type[NodeBase]:
diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py
index d7074c88d..754df3be5 100644
--- a/daemon/core/nodes/base.py
+++ b/daemon/core/nodes/base.py
@@ -14,21 +14,21 @@
 import netaddr
 
 from core import utils
-from core.configservice.dependencies import ConfigServiceDependencies
 from core.emulator.data import InterfaceData, LinkOptions
 from core.errors import CoreCommandError, CoreError
 from core.executables import BASH, MOUNT, TEST, VCMD, VNODED
 from core.nodes.interface import DEFAULT_MTU, CoreInterface
 from core.nodes.netclient import LinuxNetClient, get_net_client
+from core.services.dependencies import ServiceDependencies
 
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
     from core.emulator.distributed import DistributedServer
     from core.emulator.session import Session
-    from core.configservice.base import ConfigService
+    from core.services.base import Service
 
-    ConfigServiceType = type[ConfigService]
+    ConfigServiceType = type[Service]
 
 PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")]
 
@@ -392,7 +392,7 @@ def __init__(
             will run on, default is None for localhost
         """
         super().__init__(session, _id, name, server, options)
-        self.config_services: dict[str, "ConfigService"] = {}
+        self.config_services: dict[str, "Service"] = {}
         self.directory: Optional[Path] = None
         self.tmpnodedir: bool = False
 
@@ -498,7 +498,7 @@ def start_config_services(self) -> None:
 
         :return: nothing
         """
-        startup_paths = ConfigServiceDependencies(self.config_services).startup_paths()
+        startup_paths = ServiceDependencies(self.config_services).startup_paths()
         for startup_path in startup_paths:
             for service in startup_path:
                 service.start()
diff --git a/daemon/core/configservice/__init__.py b/daemon/core/services/__init__.py
similarity index 100%
rename from daemon/core/configservice/__init__.py
rename to daemon/core/services/__init__.py
diff --git a/daemon/core/configservice/base.py b/daemon/core/services/base.py
similarity index 94%
rename from daemon/core/configservice/base.py
rename to daemon/core/services/base.py
index e15260eb2..e0c6318da 100644
--- a/daemon/core/configservice/base.py
+++ b/daemon/core/services/base.py
@@ -33,17 +33,17 @@ def get_template_path(file_path: Path) -> str:
     return template_path
 
 
-class ConfigServiceMode(enum.Enum):
+class ServiceMode(enum.Enum):
     BLOCKING = 0
     NON_BLOCKING = 1
     TIMER = 2
 
 
-class ConfigServiceBootError(Exception):
+class ServiceBootError(Exception):
     pass
 
 
-class ConfigServiceTemplateError(Exception):
+class ServiceTemplateError(Exception):
     pass
 
 
@@ -55,9 +55,9 @@ class ShadowDir:
     has_node_paths: bool = False
 
 
-class ConfigService(abc.ABC):
+class Service(abc.ABC):
     """
-    Base class for creating configurable services.
+    Base class for creating services.
     """
 
     # validation period in seconds, how frequent validation is attempted
@@ -71,7 +71,7 @@ class ConfigService(abc.ABC):
 
     def __init__(self, node: CoreNode) -> None:
         """
-        Create ConfigService instance.
+        Create Service instance.
 
         :param node: node this service is assigned to
         """
@@ -153,7 +153,7 @@ def shutdown(self) -> list[str]:
 
     @property
     @abc.abstractmethod
-    def validation_mode(self) -> ConfigServiceMode:
+    def validation_mode(self) -> ServiceMode:
         raise NotImplementedError
 
     def start(self) -> None:
@@ -162,16 +162,16 @@ def start(self) -> None:
         validation mode.
 
         :return: nothing
-        :raises ConfigServiceBootError: when there is an error starting service
+        :raises ServiceBootError: when there is an error starting service
         """
         logger.info("node(%s) service(%s) starting...", self.node.name, self.name)
         self.create_shadow_dirs()
         self.create_dirs()
         self.create_files()
-        wait = self.validation_mode == ConfigServiceMode.BLOCKING
+        wait = self.validation_mode == ServiceMode.BLOCKING
         self.run_startup(wait)
         if not wait:
-            if self.validation_mode == ConfigServiceMode.TIMER:
+            if self.validation_mode == ServiceMode.TIMER:
                 self.wait_validation()
             else:
                 self.run_validation()
@@ -265,7 +265,7 @@ def create_dirs(self) -> None:
         :return: nothing
         :raises CoreError: when there is a failure creating a directory
         """
-        logger.debug("creating config service directories")
+        logger.debug("creating service directories")
         for directory in sorted(self.directories):
             dir_path = Path(directory)
             try:
@@ -323,7 +323,7 @@ def get_templates(self) -> dict[str, str]:
                 try:
                     template = self.get_text_template(file)
                 except Exception as e:
-                    raise ConfigServiceTemplateError(
+                    raise ServiceTemplateError(
                         f"node({self.node.name}) service({self.name}) file({file}) "
                         f"failure getting template: {e}"
                     )
@@ -351,7 +351,7 @@ def _get_rendered_template(self, file: str, data: dict[str, Any]) -> str:
             try:
                 text = self.get_text_template(file)
             except Exception as e:
-                raise ConfigServiceTemplateError(
+                raise ServiceTemplateError(
                     f"node({self.node.name}) service({self.name}) file({file}) "
                     f"failure getting template: {e}"
                 )
@@ -380,13 +380,13 @@ def run_startup(self, wait: bool) -> None:
         :param wait: wait successful command exit status when True, ignore status
             otherwise
         :return: nothing
-        :raises ConfigServiceBootError: when a command that waits fails
+        :raises ServiceBootError: when a command that waits fails
         """
         for cmd in self.startup:
             try:
                 self.node.cmd(cmd, wait=wait)
             except CoreCommandError as e:
-                raise ConfigServiceBootError(
+                raise ServiceBootError(
                     f"node({self.node.name}) service({self.name}) failed startup: {e}"
                 )
 
@@ -403,7 +403,7 @@ def run_validation(self) -> None:
         Runs validation commands for service on node.
 
         :return: nothing
-        :raises ConfigServiceBootError: if there is a validation failure
+        :raises ServiceBootError: if there is a validation failure
         """
         start = time.monotonic()
         cmds = self.validate[:]
@@ -422,7 +422,7 @@ def run_validation(self) -> None:
                 time.sleep(self.validation_period)
 
             if cmds and time.monotonic() - start > self.validation_timer:
-                raise ConfigServiceBootError(
+                raise ServiceBootError(
                     f"node({self.node.name}) service({self.name}) failed to validate"
                 )
 
@@ -460,7 +460,7 @@ def render_text(self, text: str, data: dict[str, Any] = None) -> str:
 
     def render_template(self, template_path: str, data: dict[str, Any] = None) -> str:
         """
-        Renders file based template  providing all associated data to template.
+        Renders file based template providing all associated data to template.
 
         :param template_path: path of file to render
         :param data: service specific defined data for template
diff --git a/daemon/core/configservices/__init__.py b/daemon/core/services/defaults/__init__.py
similarity index 100%
rename from daemon/core/configservices/__init__.py
rename to daemon/core/services/defaults/__init__.py
diff --git a/daemon/core/configservices/frrservices/__init__.py b/daemon/core/services/defaults/frrservices/__init__.py
similarity index 100%
rename from daemon/core/configservices/frrservices/__init__.py
rename to daemon/core/services/defaults/frrservices/__init__.py
diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/services/defaults/frrservices/services.py
similarity index 95%
rename from daemon/core/configservices/frrservices/services.py
rename to daemon/core/services/defaults/frrservices/services.py
index 378d42f84..7c331ecf3 100644
--- a/daemon/core/configservices/frrservices/services.py
+++ b/daemon/core/services/defaults/frrservices/services.py
@@ -2,13 +2,13 @@
 from typing import Any
 
 from core.config import Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode
 from core.emane.nodes import EmaneNet
 from core.nodes.base import CoreNodeBase, NodeBase
 from core.nodes.interface import DEFAULT_MTU, CoreInterface
 from core.nodes.network import PtpNet, WlanNode
 from core.nodes.physical import Rj45Node
 from core.nodes.wireless import WirelessNode
+from core.services.base import Service, ServiceMode
 
 GROUP: str = "FRR"
 FRR_STATE_DIR: str = "/var/run/frr"
@@ -79,7 +79,7 @@ def rj45_check(iface: CoreInterface) -> bool:
     return False
 
 
-class FRRZebra(ConfigService):
+class FRRZebra(Service):
     name: str = "FRRzebra"
     group: str = GROUP
     directories: list[str] = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
@@ -94,7 +94,7 @@ class FRRZebra(ConfigService):
     startup: list[str] = ["bash frrboot.sh zebra"]
     validate: list[str] = ["pidof zebra"]
     shutdown: list[str] = ["killall zebra"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -153,7 +153,7 @@ class FrrService(abc.ABC):
     startup: list[str] = []
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
     ipv4_routing: bool = False
@@ -168,7 +168,7 @@ def frr_config(self) -> str:
         raise NotImplementedError
 
 
-class FRROspfv2(FrrService, ConfigService):
+class FRROspfv2(FrrService, Service):
     """
     The OSPFv2 service provides IPv4 routing for wired networks. It does
     not build its own configuration file but has hooks for adding to the
@@ -220,7 +220,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
         return self.render_text(text, data)
 
 
-class FRROspfv3(FrrService, ConfigService):
+class FRROspfv3(FrrService, Service):
     """
     The OSPFv3 service provides IPv6 routing for wired networks. It does
     not build its own configuration file but has hooks for adding to the
@@ -257,7 +257,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
             return ""
 
 
-class FRRBgp(FrrService, ConfigService):
+class FRRBgp(FrrService, Service):
     """
     The BGP service provides interdomain routing.
     Peers must be manually configured, with a full mesh for those
@@ -289,7 +289,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class FRRRip(FrrService, ConfigService):
+class FRRRip(FrrService, Service):
     """
     The RIP service provides IPv4 routing for wired networks.
     """
@@ -314,7 +314,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class FRRRipng(FrrService, ConfigService):
+class FRRRipng(FrrService, Service):
     """
     The RIP NG service provides IPv6 routing for wired networks.
     """
@@ -339,7 +339,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class FRRBabel(FrrService, ConfigService):
+class FRRBabel(FrrService, Service):
     """
     The Babel service provides a loop-avoiding distance-vector routing
     protocol for IPv6 and IPv4 with fast convergence properties.
@@ -380,7 +380,7 @@ def frr_iface_config(self, iface: CoreInterface) -> str:
         return self.clean_text(text)
 
 
-class FRRpimd(FrrService, ConfigService):
+class FRRpimd(FrrService, Service):
     """
     PIM multicast routing based on XORP.
     """
diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/services/defaults/frrservices/templates/frrboot.sh
similarity index 100%
rename from daemon/core/configservices/frrservices/templates/frrboot.sh
rename to daemon/core/services/defaults/frrservices/templates/frrboot.sh
diff --git a/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/daemons b/daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/daemons
similarity index 100%
rename from daemon/core/configservices/frrservices/templates/usr/local/etc/frr/daemons
rename to daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/daemons
diff --git a/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/frr.conf b/daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/frr.conf
similarity index 100%
rename from daemon/core/configservices/frrservices/templates/usr/local/etc/frr/frr.conf
rename to daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/frr.conf
diff --git a/daemon/core/configservices/frrservices/templates/usr/local/etc/frr/vtysh.conf b/daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/vtysh.conf
similarity index 100%
rename from daemon/core/configservices/frrservices/templates/usr/local/etc/frr/vtysh.conf
rename to daemon/core/services/defaults/frrservices/templates/usr/local/etc/frr/vtysh.conf
diff --git a/daemon/core/configservices/nrlservices/__init__.py b/daemon/core/services/defaults/nrlservices/__init__.py
similarity index 100%
rename from daemon/core/configservices/nrlservices/__init__.py
rename to daemon/core/services/defaults/nrlservices/__init__.py
diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/services/defaults/nrlservices/services.py
similarity index 86%
rename from daemon/core/configservices/nrlservices/services.py
rename to daemon/core/services/defaults/nrlservices/services.py
index 3002cd94f..7ea5e395f 100644
--- a/daemon/core/configservices/nrlservices/services.py
+++ b/daemon/core/services/defaults/nrlservices/services.py
@@ -2,12 +2,12 @@
 
 from core import utils
 from core.config import Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode
+from core.services.base import Service, ServiceMode
 
 GROUP: str = "ProtoSvc"
 
 
-class MgenSinkService(ConfigService):
+class MgenSinkService(Service):
     name: str = "MGEN_Sink"
     group: str = GROUP
     directories: list[str] = []
@@ -17,7 +17,7 @@ class MgenSinkService(ConfigService):
     startup: list[str] = ["bash mgensink.sh"]
     validate: list[str] = ["pidof mgen"]
     shutdown: list[str] = ["killall mgen"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -29,7 +29,7 @@ def data(self) -> dict[str, Any]:
         return dict(ifnames=ifnames)
 
 
-class NrlNhdp(ConfigService):
+class NrlNhdp(Service):
     name: str = "NHDP"
     group: str = GROUP
     directories: list[str] = []
@@ -39,7 +39,7 @@ class NrlNhdp(ConfigService):
     startup: list[str] = ["bash nrlnhdp.sh"]
     validate: list[str] = ["pidof nrlnhdp"]
     shutdown: list[str] = ["killall nrlnhdp"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -51,7 +51,7 @@ def data(self) -> dict[str, Any]:
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
-class NrlSmf(ConfigService):
+class NrlSmf(Service):
     name: str = "SMF"
     group: str = GROUP
     directories: list[str] = []
@@ -61,7 +61,7 @@ class NrlSmf(ConfigService):
     startup: list[str] = ["bash startsmf.sh"]
     validate: list[str] = ["pidof nrlsmf"]
     shutdown: list[str] = ["killall nrlsmf"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -81,7 +81,7 @@ def data(self) -> dict[str, Any]:
         )
 
 
-class NrlOlsr(ConfigService):
+class NrlOlsr(Service):
     name: str = "OLSR"
     group: str = GROUP
     directories: list[str] = []
@@ -91,7 +91,7 @@ class NrlOlsr(ConfigService):
     startup: list[str] = ["bash nrlolsrd.sh"]
     validate: list[str] = ["pidof nrlolsrd"]
     shutdown: list[str] = ["killall nrlolsrd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -105,7 +105,7 @@ def data(self) -> dict[str, Any]:
         return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
 
 
-class NrlOlsrv2(ConfigService):
+class NrlOlsrv2(Service):
     name: str = "OLSRv2"
     group: str = GROUP
     directories: list[str] = []
@@ -115,7 +115,7 @@ class NrlOlsrv2(ConfigService):
     startup: list[str] = ["bash nrlolsrv2.sh"]
     validate: list[str] = ["pidof nrlolsrv2"]
     shutdown: list[str] = ["killall nrlolsrv2"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -127,7 +127,7 @@ def data(self) -> dict[str, Any]:
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
-class OlsrOrg(ConfigService):
+class OlsrOrg(Service):
     name: str = "OLSRORG"
     group: str = GROUP
     directories: list[str] = ["/etc/olsrd"]
@@ -137,7 +137,7 @@ class OlsrOrg(ConfigService):
     startup: list[str] = ["bash olsrd.sh"]
     validate: list[str] = ["pidof olsrd"]
     shutdown: list[str] = ["killall olsrd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -149,7 +149,7 @@ def data(self) -> dict[str, Any]:
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
-class MgenActor(ConfigService):
+class MgenActor(Service):
     name: str = "MgenActor"
     group: str = GROUP
     directories: list[str] = []
@@ -159,6 +159,6 @@ class MgenActor(ConfigService):
     startup: list[str] = ["bash start_mgen_actor.sh"]
     validate: list[str] = ["pidof mgen"]
     shutdown: list[str] = ["killall mgen"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
diff --git a/daemon/core/configservices/nrlservices/templates/etc/olsrd/olsrd.conf b/daemon/core/services/defaults/nrlservices/templates/etc/olsrd/olsrd.conf
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/etc/olsrd/olsrd.conf
rename to daemon/core/services/defaults/nrlservices/templates/etc/olsrd/olsrd.conf
diff --git a/daemon/core/configservices/nrlservices/templates/mgensink.sh b/daemon/core/services/defaults/nrlservices/templates/mgensink.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/mgensink.sh
rename to daemon/core/services/defaults/nrlservices/templates/mgensink.sh
diff --git a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh b/daemon/core/services/defaults/nrlservices/templates/nrlnhdp.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/nrlnhdp.sh
rename to daemon/core/services/defaults/nrlservices/templates/nrlnhdp.sh
diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrd.sh b/daemon/core/services/defaults/nrlservices/templates/nrlolsrd.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/nrlolsrd.sh
rename to daemon/core/services/defaults/nrlservices/templates/nrlolsrd.sh
diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh b/daemon/core/services/defaults/nrlservices/templates/nrlolsrv2.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh
rename to daemon/core/services/defaults/nrlservices/templates/nrlolsrv2.sh
diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.sh b/daemon/core/services/defaults/nrlservices/templates/olsrd.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/olsrd.sh
rename to daemon/core/services/defaults/nrlservices/templates/olsrd.sh
diff --git a/daemon/core/configservices/nrlservices/templates/sink.mgen b/daemon/core/services/defaults/nrlservices/templates/sink.mgen
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/sink.mgen
rename to daemon/core/services/defaults/nrlservices/templates/sink.mgen
diff --git a/daemon/core/configservices/nrlservices/templates/start_mgen_actor.sh b/daemon/core/services/defaults/nrlservices/templates/start_mgen_actor.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/start_mgen_actor.sh
rename to daemon/core/services/defaults/nrlservices/templates/start_mgen_actor.sh
diff --git a/daemon/core/configservices/nrlservices/templates/startsmf.sh b/daemon/core/services/defaults/nrlservices/templates/startsmf.sh
similarity index 100%
rename from daemon/core/configservices/nrlservices/templates/startsmf.sh
rename to daemon/core/services/defaults/nrlservices/templates/startsmf.sh
diff --git a/daemon/core/configservices/quaggaservices/__init__.py b/daemon/core/services/defaults/quaggaservices/__init__.py
similarity index 100%
rename from daemon/core/configservices/quaggaservices/__init__.py
rename to daemon/core/services/defaults/quaggaservices/__init__.py
diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/services/defaults/quaggaservices/services.py
similarity index 95%
rename from daemon/core/configservices/quaggaservices/services.py
rename to daemon/core/services/defaults/quaggaservices/services.py
index 8b4d4909c..634b7cd80 100644
--- a/daemon/core/configservices/quaggaservices/services.py
+++ b/daemon/core/services/defaults/quaggaservices/services.py
@@ -3,13 +3,13 @@
 from typing import Any
 
 from core.config import Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode
 from core.emane.nodes import EmaneNet
 from core.nodes.base import CoreNodeBase, NodeBase
 from core.nodes.interface import DEFAULT_MTU, CoreInterface
 from core.nodes.network import PtpNet, WlanNode
 from core.nodes.physical import Rj45Node
 from core.nodes.wireless import WirelessNode
+from core.services.base import Service, ServiceMode
 
 logger = logging.getLogger(__name__)
 GROUP: str = "Quagga"
@@ -81,7 +81,7 @@ def rj45_check(iface: CoreInterface) -> bool:
     return False
 
 
-class Zebra(ConfigService):
+class Zebra(Service):
     name: str = "zebra"
     group: str = GROUP
     directories: list[str] = ["/usr/local/etc/quagga", "/var/run/quagga"]
@@ -95,7 +95,7 @@ class Zebra(ConfigService):
     startup: list[str] = ["bash quaggaboot.sh zebra"]
     validate: list[str] = ["pidof zebra"]
     shutdown: list[str] = ["killall zebra"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -160,7 +160,7 @@ class QuaggaService(abc.ABC):
     startup: list[str] = []
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
     ipv4_routing: bool = False
@@ -175,7 +175,7 @@ def quagga_config(self) -> str:
         raise NotImplementedError
 
 
-class Ospfv2(QuaggaService, ConfigService):
+class Ospfv2(QuaggaService, Service):
     """
     The OSPFv2 service provides IPv4 routing for wired networks. It does
     not build its own configuration file but has hooks for adding to the
@@ -226,7 +226,7 @@ def quagga_config(self) -> str:
         return self.render_text(text, data)
 
 
-class Ospfv3(QuaggaService, ConfigService):
+class Ospfv3(QuaggaService, Service):
     """
     The OSPFv3 service provides IPv6 routing for wired networks. It does
     not build its own configuration file but has hooks for adding to the
@@ -292,7 +292,7 @@ def quagga_iface_config(self, iface: CoreInterface) -> str:
         return config
 
 
-class Bgp(QuaggaService, ConfigService):
+class Bgp(QuaggaService, Service):
     """
     The BGP service provides interdomain routing.
     Peers must be manually configured, with a full mesh for those
@@ -323,7 +323,7 @@ def quagga_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class Rip(QuaggaService, ConfigService):
+class Rip(QuaggaService, Service):
     """
     The RIP service provides IPv4 routing for wired networks.
     """
@@ -348,7 +348,7 @@ def quagga_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class Ripng(QuaggaService, ConfigService):
+class Ripng(QuaggaService, Service):
     """
     The RIP NG service provides IPv6 routing for wired networks.
     """
@@ -373,7 +373,7 @@ def quagga_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
-class Babel(QuaggaService, ConfigService):
+class Babel(QuaggaService, Service):
     """
     The Babel service provides a loop-avoiding distance-vector routing
     protocol for IPv6 and IPv4 with fast convergence properties.
@@ -414,7 +414,7 @@ def quagga_iface_config(self, iface: CoreInterface) -> str:
         return self.clean_text(text)
 
 
-class Xpimd(QuaggaService, ConfigService):
+class Xpimd(QuaggaService, Service):
     """
     PIM multicast routing based on XORP.
     """
diff --git a/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh b/daemon/core/services/defaults/quaggaservices/templates/quaggaboot.sh
similarity index 100%
rename from daemon/core/configservices/quaggaservices/templates/quaggaboot.sh
rename to daemon/core/services/defaults/quaggaservices/templates/quaggaboot.sh
diff --git a/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf b/daemon/core/services/defaults/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf
similarity index 100%
rename from daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf
rename to daemon/core/services/defaults/quaggaservices/templates/usr/local/etc/quagga/Quagga.conf
diff --git a/daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf b/daemon/core/services/defaults/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf
similarity index 100%
rename from daemon/core/configservices/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf
rename to daemon/core/services/defaults/quaggaservices/templates/usr/local/etc/quagga/vtysh.conf
diff --git a/daemon/core/configservices/securityservices/__init__.py b/daemon/core/services/defaults/securityservices/__init__.py
similarity index 100%
rename from daemon/core/configservices/securityservices/__init__.py
rename to daemon/core/services/defaults/securityservices/__init__.py
diff --git a/daemon/core/configservices/securityservices/services.py b/daemon/core/services/defaults/securityservices/services.py
similarity index 85%
rename from daemon/core/configservices/securityservices/services.py
rename to daemon/core/services/defaults/securityservices/services.py
index ee975ed7d..8b67e077f 100644
--- a/daemon/core/configservices/securityservices/services.py
+++ b/daemon/core/services/defaults/securityservices/services.py
@@ -2,12 +2,12 @@
 
 from core import constants
 from core.config import ConfigString, Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode
+from core.services.base import Service, ServiceMode
 
 GROUP_NAME: str = "Security"
 
 
-class VpnClient(ConfigService):
+class VpnClient(Service):
     name: str = "VPNClient"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -17,7 +17,7 @@ class VpnClient(ConfigService):
     startup: list[str] = ["bash vpnclient.sh"]
     validate: list[str] = ["pidof openvpn"]
     shutdown: list[str] = ["killall openvpn"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = [
         ConfigString(
             id="keydir", label="Key Dir", default=f"{constants.CORE_CONF_DIR}/keys"
@@ -28,7 +28,7 @@ class VpnClient(ConfigService):
     modes: dict[str, dict[str, str]] = {}
 
 
-class VpnServer(ConfigService):
+class VpnServer(Service):
     name: str = "VPNServer"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -38,7 +38,7 @@ class VpnServer(ConfigService):
     startup: list[str] = ["bash vpnserver.sh"]
     validate: list[str] = ["pidof openvpn"]
     shutdown: list[str] = ["killall openvpn"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = [
         ConfigString(
             id="keydir", label="Key Dir", default=f"{constants.CORE_CONF_DIR}/keys"
@@ -58,7 +58,7 @@ def data(self) -> dict[str, Any]:
         return dict(address=address)
 
 
-class IPsec(ConfigService):
+class IPsec(Service):
     name: str = "IPsec"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -68,12 +68,12 @@ class IPsec(ConfigService):
     startup: list[str] = ["bash ipsec.sh"]
     validate: list[str] = ["pidof racoon"]
     shutdown: list[str] = ["killall racoon"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
 
-class Firewall(ConfigService):
+class Firewall(Service):
     name: str = "Firewall"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -83,12 +83,12 @@ class Firewall(ConfigService):
     startup: list[str] = ["bash firewall.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
 
-class Nat(ConfigService):
+class Nat(Service):
     name: str = "NAT"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -98,7 +98,7 @@ class Nat(ConfigService):
     startup: list[str] = ["bash nat.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
diff --git a/daemon/core/configservices/securityservices/templates/firewall.sh b/daemon/core/services/defaults/securityservices/templates/firewall.sh
similarity index 100%
rename from daemon/core/configservices/securityservices/templates/firewall.sh
rename to daemon/core/services/defaults/securityservices/templates/firewall.sh
diff --git a/daemon/core/configservices/securityservices/templates/ipsec.sh b/daemon/core/services/defaults/securityservices/templates/ipsec.sh
similarity index 100%
rename from daemon/core/configservices/securityservices/templates/ipsec.sh
rename to daemon/core/services/defaults/securityservices/templates/ipsec.sh
diff --git a/daemon/core/configservices/securityservices/templates/nat.sh b/daemon/core/services/defaults/securityservices/templates/nat.sh
similarity index 100%
rename from daemon/core/configservices/securityservices/templates/nat.sh
rename to daemon/core/services/defaults/securityservices/templates/nat.sh
diff --git a/daemon/core/configservices/securityservices/templates/vpnclient.sh b/daemon/core/services/defaults/securityservices/templates/vpnclient.sh
similarity index 100%
rename from daemon/core/configservices/securityservices/templates/vpnclient.sh
rename to daemon/core/services/defaults/securityservices/templates/vpnclient.sh
diff --git a/daemon/core/configservices/securityservices/templates/vpnserver.sh b/daemon/core/services/defaults/securityservices/templates/vpnserver.sh
similarity index 100%
rename from daemon/core/configservices/securityservices/templates/vpnserver.sh
rename to daemon/core/services/defaults/securityservices/templates/vpnserver.sh
diff --git a/daemon/core/configservices/utilservices/__init__.py b/daemon/core/services/defaults/utilservices/__init__.py
similarity index 100%
rename from daemon/core/configservices/utilservices/__init__.py
rename to daemon/core/services/defaults/utilservices/__init__.py
diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/services/defaults/utilservices/services.py
similarity index 86%
rename from daemon/core/configservices/utilservices/services.py
rename to daemon/core/services/defaults/utilservices/services.py
index 73d720608..1a216ff6e 100644
--- a/daemon/core/configservices/utilservices/services.py
+++ b/daemon/core/services/defaults/utilservices/services.py
@@ -4,12 +4,12 @@
 
 from core import utils
 from core.config import Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode
+from core.services.base import Service, ServiceMode
 
 GROUP_NAME = "Utility"
 
 
-class DefaultRouteService(ConfigService):
+class DefaultRouteService(Service):
     name: str = "DefaultRoute"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -19,7 +19,7 @@ class DefaultRouteService(ConfigService):
     startup: list[str] = ["bash defaultroute.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -37,7 +37,7 @@ def data(self) -> dict[str, Any]:
         return dict(routes=routes)
 
 
-class DefaultMulticastRouteService(ConfigService):
+class DefaultMulticastRouteService(Service):
     name: str = "DefaultMulticastRoute"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -47,7 +47,7 @@ class DefaultMulticastRouteService(ConfigService):
     startup: list[str] = ["bash defaultmroute.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -59,7 +59,7 @@ def data(self) -> dict[str, Any]:
         return dict(ifname=ifname)
 
 
-class StaticRouteService(ConfigService):
+class StaticRouteService(Service):
     name: str = "StaticRoute"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -69,7 +69,7 @@ class StaticRouteService(ConfigService):
     startup: list[str] = ["bash staticroute.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -87,7 +87,7 @@ def data(self) -> dict[str, Any]:
         return dict(routes=routes)
 
 
-class IpForwardService(ConfigService):
+class IpForwardService(Service):
     name: str = "IPForward"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -97,7 +97,7 @@ class IpForwardService(ConfigService):
     startup: list[str] = ["bash ipforward.sh"]
     validate: list[str] = []
     shutdown: list[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -109,7 +109,7 @@ def data(self) -> dict[str, Any]:
         return dict(devnames=devnames)
 
 
-class SshService(ConfigService):
+class SshService(Service):
     name: str = "SSH"
     group: str = GROUP_NAME
     directories: list[str] = ["/etc/ssh", "/var/run/sshd"]
@@ -119,7 +119,7 @@ class SshService(ConfigService):
     startup: list[str] = ["bash startsshd.sh"]
     validate: list[str] = []
     shutdown: list[str] = ["killall sshd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -131,7 +131,7 @@ def data(self) -> dict[str, Any]:
         )
 
 
-class DhcpService(ConfigService):
+class DhcpService(Service):
     name: str = "DHCP"
     group: str = GROUP_NAME
     directories: list[str] = ["/etc/dhcp", "/var/lib/dhcp"]
@@ -141,7 +141,7 @@ class DhcpService(ConfigService):
     startup: list[str] = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
     validate: list[str] = ["pidof dhcpd"]
     shutdown: list[str] = ["killall dhcpd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -159,7 +159,7 @@ def data(self) -> dict[str, Any]:
         return dict(subnets=subnets)
 
 
-class DhcpClientService(ConfigService):
+class DhcpClientService(Service):
     name: str = "DHCPClient"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -169,7 +169,7 @@ class DhcpClientService(ConfigService):
     startup: list[str] = ["bash startdhcpclient.sh"]
     validate: list[str] = ["pidof dhclient"]
     shutdown: list[str] = ["killall dhclient"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -180,7 +180,7 @@ def data(self) -> dict[str, Any]:
         return dict(ifnames=ifnames)
 
 
-class FtpService(ConfigService):
+class FtpService(Service):
     name: str = "FTP"
     group: str = GROUP_NAME
     directories: list[str] = ["/var/run/vsftpd/empty", "/var/ftp"]
@@ -190,12 +190,12 @@ class FtpService(ConfigService):
     startup: list[str] = ["vsftpd ./vsftpd.conf"]
     validate: list[str] = ["pidof vsftpd"]
     shutdown: list[str] = ["killall vsftpd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
 
-class PcapService(ConfigService):
+class PcapService(Service):
     name: str = "pcap"
     group: str = GROUP_NAME
     directories: list[str] = []
@@ -205,7 +205,7 @@ class PcapService(ConfigService):
     startup: list[str] = ["bash pcap.sh start"]
     validate: list[str] = ["pidof tcpdump"]
     shutdown: list[str] = ["bash pcap.sh stop"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -216,7 +216,7 @@ def data(self) -> dict[str, Any]:
         return dict(ifnames=ifnames)
 
 
-class RadvdService(ConfigService):
+class RadvdService(Service):
     name: str = "radvd"
     group: str = GROUP_NAME
     directories: list[str] = ["/etc/radvd", "/var/run/radvd"]
@@ -228,7 +228,7 @@ class RadvdService(ConfigService):
     ]
     validate: list[str] = ["pidof radvd"]
     shutdown: list[str] = ["pkill radvd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
@@ -244,7 +244,7 @@ def data(self) -> dict[str, Any]:
         return dict(ifaces=ifaces)
 
 
-class AtdService(ConfigService):
+class AtdService(Service):
     name: str = "atd"
     group: str = GROUP_NAME
     directories: list[str] = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
@@ -254,12 +254,12 @@ class AtdService(ConfigService):
     startup: list[str] = ["bash startatd.sh"]
     validate: list[str] = ["pidof atd"]
     shutdown: list[str] = ["pkill atd"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
 
-class HttpService(ConfigService):
+class HttpService(Service):
     name: str = "HTTP"
     group: str = GROUP_NAME
     directories: list[str] = [
@@ -280,7 +280,7 @@ class HttpService(ConfigService):
     startup: list[str] = ["chown www-data /var/lock/apache2", "apache2ctl start"]
     validate: list[str] = ["pidof apache2"]
     shutdown: list[str] = ["apache2ctl stop"]
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: list[Configuration] = []
     modes: dict[str, dict[str, str]] = {}
 
diff --git a/daemon/core/configservices/utilservices/templates/defaultmroute.sh b/daemon/core/services/defaults/utilservices/templates/defaultmroute.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/defaultmroute.sh
rename to daemon/core/services/defaults/utilservices/templates/defaultmroute.sh
diff --git a/daemon/core/configservices/utilservices/templates/defaultroute.sh b/daemon/core/services/defaults/utilservices/templates/defaultroute.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/defaultroute.sh
rename to daemon/core/services/defaults/utilservices/templates/defaultroute.sh
diff --git a/daemon/core/configservices/utilservices/templates/etc/apache2/apache2.conf b/daemon/core/services/defaults/utilservices/templates/etc/apache2/apache2.conf
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/etc/apache2/apache2.conf
rename to daemon/core/services/defaults/utilservices/templates/etc/apache2/apache2.conf
diff --git a/daemon/core/configservices/utilservices/templates/etc/apache2/envvars b/daemon/core/services/defaults/utilservices/templates/etc/apache2/envvars
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/etc/apache2/envvars
rename to daemon/core/services/defaults/utilservices/templates/etc/apache2/envvars
diff --git a/daemon/core/configservices/utilservices/templates/etc/dhcp/dhcpd.conf b/daemon/core/services/defaults/utilservices/templates/etc/dhcp/dhcpd.conf
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/etc/dhcp/dhcpd.conf
rename to daemon/core/services/defaults/utilservices/templates/etc/dhcp/dhcpd.conf
diff --git a/daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf b/daemon/core/services/defaults/utilservices/templates/etc/radvd/radvd.conf
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/etc/radvd/radvd.conf
rename to daemon/core/services/defaults/utilservices/templates/etc/radvd/radvd.conf
diff --git a/daemon/core/configservices/utilservices/templates/etc/ssh/sshd_config b/daemon/core/services/defaults/utilservices/templates/etc/ssh/sshd_config
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/etc/ssh/sshd_config
rename to daemon/core/services/defaults/utilservices/templates/etc/ssh/sshd_config
diff --git a/daemon/core/configservices/utilservices/templates/ipforward.sh b/daemon/core/services/defaults/utilservices/templates/ipforward.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/ipforward.sh
rename to daemon/core/services/defaults/utilservices/templates/ipforward.sh
diff --git a/daemon/core/configservices/utilservices/templates/pcap.sh b/daemon/core/services/defaults/utilservices/templates/pcap.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/pcap.sh
rename to daemon/core/services/defaults/utilservices/templates/pcap.sh
diff --git a/daemon/core/configservices/utilservices/templates/startatd.sh b/daemon/core/services/defaults/utilservices/templates/startatd.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/startatd.sh
rename to daemon/core/services/defaults/utilservices/templates/startatd.sh
diff --git a/daemon/core/configservices/utilservices/templates/startdhcpclient.sh b/daemon/core/services/defaults/utilservices/templates/startdhcpclient.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/startdhcpclient.sh
rename to daemon/core/services/defaults/utilservices/templates/startdhcpclient.sh
diff --git a/daemon/core/configservices/utilservices/templates/startsshd.sh b/daemon/core/services/defaults/utilservices/templates/startsshd.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/startsshd.sh
rename to daemon/core/services/defaults/utilservices/templates/startsshd.sh
diff --git a/daemon/core/configservices/utilservices/templates/staticroute.sh b/daemon/core/services/defaults/utilservices/templates/staticroute.sh
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/staticroute.sh
rename to daemon/core/services/defaults/utilservices/templates/staticroute.sh
diff --git a/daemon/core/configservices/utilservices/templates/var/www/index.html b/daemon/core/services/defaults/utilservices/templates/var/www/index.html
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/var/www/index.html
rename to daemon/core/services/defaults/utilservices/templates/var/www/index.html
diff --git a/daemon/core/configservices/utilservices/templates/vsftpd.conf b/daemon/core/services/defaults/utilservices/templates/vsftpd.conf
similarity index 100%
rename from daemon/core/configservices/utilservices/templates/vsftpd.conf
rename to daemon/core/services/defaults/utilservices/templates/vsftpd.conf
diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/services/dependencies.py
similarity index 84%
rename from daemon/core/configservice/dependencies.py
rename to daemon/core/services/dependencies.py
index 1fbc4e48d..492718a7f 100644
--- a/daemon/core/configservice/dependencies.py
+++ b/daemon/core/services/dependencies.py
@@ -4,24 +4,24 @@
 logger = logging.getLogger(__name__)
 
 if TYPE_CHECKING:
-    from core.configservice.base import ConfigService
+    from core.services.base import Service
 
 
-class ConfigServiceDependencies:
+class ServiceDependencies:
     """
     Generates sets of services to start in order of their dependencies.
     """
 
-    def __init__(self, services: dict[str, "ConfigService"]) -> None:
+    def __init__(self, services: dict[str, "Service"]) -> None:
         """
-        Create a ConfigServiceDependencies instance.
+        Create a ServiceDependencies instance.
 
         :param services: services for determining dependency sets
         """
         # helpers to check validity
         self.dependents: dict[str, set[str]] = {}
         self.started: set[str] = set()
-        self.node_services: dict[str, "ConfigService"] = {}
+        self.node_services: dict[str, "Service"] = {}
         for service in services.values():
             self.node_services[service.name] = service
             for dependency in service.dependencies:
@@ -29,15 +29,15 @@ def __init__(self, services: dict[str, "ConfigService"]) -> None:
                 dependents.add(service.name)
 
         # used to find paths
-        self.path: list["ConfigService"] = []
+        self.path: list["Service"] = []
         self.visited: set[str] = set()
         self.visiting: set[str] = set()
 
-    def startup_paths(self) -> list[list["ConfigService"]]:
+    def startup_paths(self) -> list[list["Service"]]:
         """
         Find startup path sets based on service dependencies.
 
-        :return: lists of lists of services that can be started in parallel
+        :return: list of lists of services that can be started in parallel
         """
         paths = []
         for name in self.node_services:
@@ -70,9 +70,9 @@ def _reset(self) -> None:
         self.visited.clear()
         self.visiting.clear()
 
-    def _start(self, service: "ConfigService") -> list["ConfigService"]:
+    def _start(self, service: "Service") -> list["Service"]:
         """
-        Starts a oath for checking dependencies for a given service.
+        Starts a path for checking dependencies for a given service.
 
         :param service: service to check dependencies for
         :return: list of config services to start in order
@@ -81,7 +81,7 @@ def _start(self, service: "ConfigService") -> list["ConfigService"]:
         self._reset()
         return self._visit(service)
 
-    def _visit(self, current_service: "ConfigService") -> list["ConfigService"]:
+    def _visit(self, current_service: "Service") -> list["Service"]:
         """
         Visits a service when discovering dependency chains for service.
 
diff --git a/daemon/core/configservice/manager.py b/daemon/core/services/manager.py
similarity index 72%
rename from daemon/core/configservice/manager.py
rename to daemon/core/services/manager.py
index 09b729851..c00aab5cd 100644
--- a/daemon/core/configservice/manager.py
+++ b/daemon/core/services/manager.py
@@ -3,23 +3,24 @@
 import pkgutil
 from pathlib import Path
 
-from core import configservices, utils
-from core.configservice.base import ConfigService
+from core import utils
 from core.errors import CoreError
+from core.services import defaults
+from core.services.base import Service
 
 logger = logging.getLogger(__name__)
 
 
-class ConfigServiceManager:
+class ServiceManager:
     """
-    Manager for configurable services.
+    Manager for services.
     """
 
     def __init__(self):
         """
-        Create a ConfigServiceManager instance.
+        Create a ServiceManager instance.
         """
-        self.services: dict[str, type[ConfigService]] = {}
+        self.services: dict[str, type[Service]] = {}
         self.defaults: dict[str, list[str]] = {
             "mdr": ["zebra", "OSPFv3MDR", "IPForward"],
             "PC": ["DefaultRoute"],
@@ -28,7 +29,7 @@ def __init__(self):
             "host": ["DefaultRoute", "SSH"],
         }
 
-    def get_service(self, name: str) -> type[ConfigService]:
+    def get_service(self, name: str) -> type[Service]:
         """
         Retrieve a service by name.
 
@@ -41,7 +42,7 @@ def get_service(self, name: str) -> type[ConfigService]:
             raise CoreError(f"service does not exist {name}")
         return service_class
 
-    def add(self, service: type[ConfigService]) -> None:
+    def add(self, service: type[Service]) -> None:
         """
         Add service to manager, checking service requirements have been met.
 
@@ -63,35 +64,35 @@ def add(self, service: type[ConfigService]) -> None:
             try:
                 utils.which(executable, required=True)
             except CoreError as e:
-                raise CoreError(f"config service({service.name}): {e}")
+                raise CoreError(f"service({service.name}): {e}")
 
         # make service available
         self.services[name] = service
 
     def load_locals(self) -> list[str]:
         """
-        Search and add config service from local core module.
+        Search and add service from local core module.
 
         :return: list of errors when loading services
         """
         errors = []
         for module_info in pkgutil.walk_packages(
-            configservices.__path__, f"{configservices.__name__}."
+            defaults.__path__, f"{defaults.__name__}."
         ):
-            services = utils.load_module(module_info.name, ConfigService)
+            services = utils.load_module(module_info.name, Service)
             for service in services:
                 try:
                     self.add(service)
                 except CoreError as e:
                     errors.append(service.name)
-                    logger.debug("not loading config service(%s): %s", service.name, e)
+                    logger.debug("not loading service(%s): %s", service.name, e)
         return errors
 
     def load(self, path: Path) -> list[str]:
         """
-        Search path provided for config services and add them for being managed.
+        Search path provided for services and add them for being managed.
 
-        :param path: path to search configurable services
+        :param path: path to search services
         :return: list errors when loading services
         """
         path = pathlib.Path(path)
@@ -99,8 +100,8 @@ def load(self, path: Path) -> list[str]:
         subdirs.append(path)
         service_errors = []
         for subdir in subdirs:
-            logger.debug("loading config services from: %s", subdir)
-            services = utils.load_classes(subdir, ConfigService)
+            logger.debug("loading services from: %s", subdir)
+            services = utils.load_classes(subdir, Service)
             for service in services:
                 try:
                     self.add(service)
diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py
index cee7fae16..a878b3308 100644
--- a/daemon/tests/test_grpc.py
+++ b/daemon/tests/test_grpc.py
@@ -31,7 +31,6 @@
     SessionLocation,
     SessionState,
 )
-from core.configservices.utilservices.services import DefaultRouteService
 from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
 from core.emulator.data import EventData, IpPrefixes, NodeData
@@ -40,6 +39,7 @@
 from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode, WlanNode
+from core.services.defaults.utilservices.services import DefaultRouteService
 from core.xml.corexml import CoreXmlWriter
 
 
diff --git a/daemon/tests/test_config_services.py b/daemon/tests/test_services.py
similarity index 91%
rename from daemon/tests/test_config_services.py
rename to daemon/tests/test_services.py
index 876b7f320..a7fe987e3 100644
--- a/daemon/tests/test_config_services.py
+++ b/daemon/tests/test_services.py
@@ -4,17 +4,13 @@
 import pytest
 
 from core.config import ConfigBool, ConfigString
-from core.configservice.base import (
-    ConfigService,
-    ConfigServiceBootError,
-    ConfigServiceMode,
-)
 from core.errors import CoreCommandError, CoreError
+from core.services.base import Service, ServiceBootError, ServiceMode
 
 TEMPLATE_TEXT = "echo hello"
 
 
-class MyService(ConfigService):
+class MyService(Service):
     name = "MyService"
     group = "MyGroup"
     directories = ["/usr/local/lib"]
@@ -24,7 +20,7 @@ class MyService(ConfigService):
     startup = [f"sh {files[0]}"]
     validate = [f"pidof {files[0]}"]
     shutdown = [f"pkill {files[0]}"]
-    validation_mode = ConfigServiceMode.BLOCKING
+    validation_mode = ServiceMode.BLOCKING
     default_configs = [
         ConfigString(id="value1", label="Text"),
         ConfigBool(id="value2", label="Boolean"),
@@ -42,7 +38,7 @@ def get_text_template(self, name: str) -> str:
         return TEMPLATE_TEXT
 
 
-class TestConfigServices:
+class TestServices:
     def test_set_template(self):
         # given
         node = mock.MagicMock()
@@ -113,7 +109,7 @@ def test_run_startup_exception(self):
         service = MyService(node)
 
         # when
-        with pytest.raises(ConfigServiceBootError):
+        with pytest.raises(ServiceBootError):
             service.run_startup(wait=True)
 
     def test_shutdown(self):
@@ -142,7 +138,7 @@ def test_run_validation_timer(self):
         # given
         node = mock.MagicMock()
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.TIMER
+        service.validation_mode = ServiceMode.TIMER
         service.validation_timer = 0
 
         # when
@@ -156,19 +152,19 @@ def test_run_validation_timer_exception(self):
         node = mock.MagicMock()
         node.cmd.side_effect = CoreCommandError(1, "error")
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.TIMER
+        service.validation_mode = ServiceMode.TIMER
         service.validation_period = 0
         service.validation_timer = 0
 
         # when
-        with pytest.raises(ConfigServiceBootError):
+        with pytest.raises(ServiceBootError):
             service.run_validation()
 
     def test_run_validation_non_blocking(self):
         # given
         node = mock.MagicMock()
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.NON_BLOCKING
+        service.validation_mode = ServiceMode.NON_BLOCKING
         service.validation_period = 0
         service.validation_timer = 0
 
@@ -183,12 +179,12 @@ def test_run_validation_non_blocking_exception(self):
         node = mock.MagicMock()
         node.cmd.side_effect = CoreCommandError(1, "error")
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.NON_BLOCKING
+        service.validation_mode = ServiceMode.NON_BLOCKING
         service.validation_period = 0
         service.validation_timer = 0
 
         # when
-        with pytest.raises(ConfigServiceBootError):
+        with pytest.raises(ServiceBootError):
             service.run_validation()
 
     def test_render_config(self):
@@ -261,7 +257,7 @@ def test_start_timer(self):
         # given
         node = mock.MagicMock()
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.TIMER
+        service.validation_mode = ServiceMode.TIMER
         service.create_dirs = mock.MagicMock()
         service.create_files = mock.MagicMock()
         service.run_startup = mock.MagicMock()
@@ -282,7 +278,7 @@ def test_start_non_blocking(self):
         # given
         node = mock.MagicMock()
         service = MyService(node)
-        service.validation_mode = ConfigServiceMode.NON_BLOCKING
+        service.validation_mode = ServiceMode.NON_BLOCKING
         service.create_dirs = mock.MagicMock()
         service.create_files = mock.MagicMock()
         service.run_startup = mock.MagicMock()
diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py
index c1787b77f..9470a42f6 100644
--- a/daemon/tests/test_xml.py
+++ b/daemon/tests/test_xml.py
@@ -4,7 +4,6 @@
 
 import pytest
 
-from core.configservices.utilservices.services import DefaultRouteService
 from core.emulator.data import IpPrefixes, LinkOptions
 from core.emulator.enumerations import EventTypes
 from core.emulator.session import Session
@@ -12,6 +11,7 @@
 from core.location.mobility import BasicRangeModel
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode, WlanNode
+from core.services.defaults.utilservices.services import DefaultRouteService
 
 
 class TestXml:
diff --git a/docs/configservices.md b/docs/configservices.md
index a82c8c59c..a2564642a 100644
--- a/docs/configservices.md
+++ b/docs/configservices.md
@@ -115,11 +115,11 @@ running the shell files generated.
 from typing import Dict, List
 
 from core.config import ConfigString, ConfigBool, Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode, ShadowDir
+from core.services.base import Service, ServiceMode, ShadowDir
 
 
 # class that subclasses ConfigService
-class ExampleService(ConfigService):
+class ExampleService(Service):
     # unique name for your service within CORE
     name: str = "Example"
     # the group your service is associated with, used for display in GUI
@@ -145,7 +145,7 @@ class ExampleService(ConfigService):
     # commands to run to stop this service
     shutdown: List[str] = []
     # validation mode, blocking, non-blocking, and timer
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     # configurable values that this service can use, for file generation
     default_configs: List[Configuration] = [
         ConfigString(id="value1", label="Text"),
diff --git a/package/share/tutorials/chatapp/chatapp_service.py b/package/share/tutorials/chatapp/chatapp_service.py
index 6faf80711..ddc988cd0 100644
--- a/package/share/tutorials/chatapp/chatapp_service.py
+++ b/package/share/tutorials/chatapp/chatapp_service.py
@@ -1,10 +1,10 @@
 from typing import Dict, List
 
 from core.config import Configuration
-from core.configservice.base import ConfigService, ConfigServiceMode, ShadowDir
+from core.services.base import Service, ServiceMode, ShadowDir
 
 
-class ChatAppService(ConfigService):
+class ChatAppService(Service):
     name: str = "ChatApp Server"
     group: str = "ChatApp"
     directories: List[str] = []
@@ -14,7 +14,7 @@ class ChatAppService(ConfigService):
     startup: List[str] = [f"bash {files[0]}"]
     validate: List[str] = []
     shutdown: List[str] = []
-    validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
     default_configs: List[Configuration] = []
     modes: Dict[str, Dict[str, str]] = {}
     shadow_directories: List[ShadowDir] = []

From d16012e555470f51d64c64eb1c5f9095a633db6b Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Tue, 26 Sep 2023 13:22:36 -0700
Subject: [PATCH 6/7] daemon/docs: adjustments to update code, doc, and comment
 references for config services, to just be services

---
 daemon/core/api/grpc/client.py                |  20 +-
 daemon/core/api/grpc/grpcutils.py             |  24 +-
 daemon/core/api/grpc/server.py                |  36 +-
 daemon/core/emulator/coreemu.py               |   6 +-
 daemon/core/emulator/data.py                  |   2 -
 daemon/core/emulator/session.py               |  10 +-
 daemon/core/gui/coreclient.py                 |  20 +-
 daemon/core/gui/dialogs/customnodes.py        |   8 +-
 .../{nodeconfigservice.py => nodeservice.py}  |  16 +-
 ...onfigserviceconfig.py => serviceconfig.py} |  22 +-
 daemon/core/gui/graph/node.py                 |  12 +-
 daemon/core/nodes/base.py                     |  54 ++-
 .../services/defaults/frrservices/services.py |   2 +-
 .../services/defaults/nrlservices/services.py |  14 +-
 .../defaults/quaggaservices/services.py       |   2 +-
 daemon/core/services/dependencies.py          |   2 +-
 daemon/core/xml/corexml.py                    |  43 +--
 daemon/pyproject.toml                         |   2 +-
 daemon/tests/test_grpc.py                     |   6 +-
 daemon/tests/test_xml.py                      |   4 +-
 docs/configservices.md                        | 196 ----------
 docs/services.md                              | 341 ++++++------------
 docs/tutorials/setup.md                       |   4 +-
 mkdocs.yml                                    |   3 +-
 package/etc/core.conf                         |   1 -
 .../share/examples/configservices/switch.py   |   2 +-
 26 files changed, 264 insertions(+), 588 deletions(-)
 rename daemon/core/gui/dialogs/{nodeconfigservice.py => nodeservice.py} (92%)
 rename daemon/core/gui/dialogs/{configserviceconfig.py => serviceconfig.py} (96%)
 delete mode 100644 docs/configservices.md

diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index 575501f8e..5768a0837 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -718,7 +718,7 @@ def get_config(self) -> wrappers.CoreConfig:
         response = self.stub.GetConfig(request)
         return wrappers.CoreConfig.from_proto(response)
 
-    def config_service_action(
+    def service_action(
         self,
         session_id: int,
         node_id: int,
@@ -726,11 +726,11 @@ def config_service_action(
         action: wrappers.ServiceAction,
     ) -> bool:
         """
-        Send an action to a config service for a node.
+        Send an action to a service for a node.
 
         :param session_id: session id
         :param node_id: node id
-        :param service: config service name
+        :param service: service name
         :param action: action for service (start, stop, restart,
             validate)
         :return: True for success, False otherwise
@@ -866,16 +866,16 @@ def get_ifaces(self) -> list[str]:
         response = self.stub.GetInterfaces(request)
         return list(response.ifaces)
 
-    def get_config_service_defaults(
+    def get_service_defaults(
         self, session_id: int, node_id: int, name: str
     ) -> wrappers.ServiceDefaults:
         """
-        Retrieves config service default values.
+        Retrieves service default values.
 
         :param session_id: session id to get node from
         :param node_id: node id to get service data from
         :param name: name of service to get defaults for
-        :return: config service defaults
+        :return: service defaults
         """
         request = GetServiceDefaultsRequest(
             name=name, session_id=session_id, node_id=node_id
@@ -883,11 +883,11 @@ def get_config_service_defaults(
         response = self.stub.GetServiceDefaults(request)
         return wrappers.ServiceDefaults.from_proto(response)
 
-    def get_node_config_service(
+    def get_node_service(
         self, session_id: int, node_id: int, name: str
     ) -> dict[str, str]:
         """
-        Retrieves information for a specific config service on a node.
+        Retrieves information for a specific service on a node.
 
         :param session_id: session node belongs to
         :param node_id: id of node to get service information from
@@ -901,11 +901,11 @@ def get_node_config_service(
         response = self.stub.GetNodeService(request)
         return dict(response.config)
 
-    def get_config_service_rendered(
+    def get_service_rendered(
         self, session_id: int, node_id: int, name: str
     ) -> dict[str, str]:
         """
-        Retrieve the rendered config service files for a node.
+        Retrieve the rendered service files for a node.
 
         :param session_id: id of session
         :param node_id: id of node
diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py
index 6b6fa9cef..e6610ccd6 100644
--- a/daemon/core/api/grpc/grpcutils.py
+++ b/daemon/core/api/grpc/grpcutils.py
@@ -72,7 +72,7 @@ def add_node_data(
     options.canvas = node_proto.canvas
     if isinstance(options, CoreNodeOptions):
         options.model = node_proto.model
-        options.config_services = node_proto.services
+        options.services = node_proto.services
     if isinstance(options, EmaneOptions):
         options.emane_model = node_proto.emane
     if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
@@ -295,10 +295,10 @@ def get_node_proto(
         lat=node.position.lat, lon=node.position.lon, alt=node.position.alt
     )
     node_dir = None
-    config_services = []
+    services = []
     if isinstance(node, CoreNodeBase):
         node_dir = str(node.directory)
-        config_services = [x for x in node.config_services]
+        services = [x for x in node.services]
     channel = None
     if isinstance(node, CoreNode):
         channel = str(node.ctrlchnlname)
@@ -335,15 +335,13 @@ def get_node_proto(
     )
     if mobility_config:
         mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility)
-    # check for config service configs
-    config_service_configs = {}
+    # check for service configs
+    service_configs = {}
     if isinstance(node, CoreNode):
-        for service in node.config_services.values():
+        for service in node.services.values():
             if not service.custom_templates and not service.custom_config:
                 continue
-            config_service_configs[service.name] = services_pb2.ServiceConfig(
-                node_id=node.id,
-                name=service.name,
+            service_configs[service.name] = services_pb2.ServiceConfig(
                 templates=service.custom_templates,
                 config=service.custom_config,
             )
@@ -357,14 +355,14 @@ def get_node_proto(
         geo=geo,
         icon=node.icon,
         image=image,
-        services=config_services,
+        services=services,
         dir=node_dir,
         channel=channel,
         canvas=node.canvas,
         wlan_config=wlan_config,
         wireless_config=wireless_config,
         mobility_config=mobility_config,
-        service_configs=config_service_configs,
+        service_configs=service_configs,
         emane_configs=emane_configs,
     )
 
@@ -806,10 +804,10 @@ def configure_node(
         if not isinstance(core_node, CoreNode):
             context.abort(
                 grpc.StatusCode.INVALID_ARGUMENT,
-                "invalid node type with config service configs",
+                "invalid node type with service configs",
             )
         for service_name, service_config in node.service_configs.items():
-            service = core_node.config_services[service_name]
+            service = core_node.services[service_name]
             if service_config.config:
                 service.set_config(dict(service_config.config))
             for name, template in service_config.templates.items():
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index be31442b1..610409581 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -211,7 +211,7 @@ def validate_service(self, name: str, context: ServicerContext) -> type[Service]
     def GetConfig(
         self, request: core_pb2.GetConfigRequest, context: ServicerContext
     ) -> core_pb2.GetConfigResponse:
-        config_services = []
+        services = []
         for service in self.coreemu.service_manager.services.values():
             service_proto = services_pb2.Service(
                 name=service.name,
@@ -227,10 +227,10 @@ def GetConfig(
                 validation_timer=service.validation_timer,
                 validation_period=service.validation_period,
             )
-            config_services.append(service_proto)
+            services.append(service_proto)
         emane_models = [x.name for x in EmaneModelManager.models.values()]
         return core_pb2.GetConfigResponse(
-            services=config_services,
+            services=services,
             emane_models=emane_models,
         )
 
@@ -900,7 +900,7 @@ def ServiceAction(
         self, request: ServiceActionRequest, context: ServicerContext
     ) -> ServiceActionResponse:
         """
-        Take action whether to start, stop, restart, validate the config service or
+        Take action whether to start, stop, restart, validate the service or
         none of the above.
 
         :param request: service action request
@@ -910,9 +910,11 @@ def ServiceAction(
         logger.debug("service action: %s", request)
         session = self.get_session(request.session_id, context)
         node = self.get_node(session, request.node_id, context, CoreNode)
-        service = node.config_services.get(request.service)
+        service = node.services.get(request.service)
         if not service:
-            context.abort(grpc.StatusCode.NOT_FOUND, "config service not found")
+            context.abort(
+                grpc.StatusCode.NOT_FOUND, f"service({request.service}) not found"
+            )
         result = False
         if request.action == ServiceAction.START:
             try:
@@ -1100,16 +1102,16 @@ def GetNodeService(
         self, request: GetNodeServiceRequest, context: ServicerContext
     ) -> GetNodeServiceResponse:
         """
-        Gets configuration, for a given configuration service, for a given node.
+        Gets configuration, for a given service, for a given node.
 
-        :param request: get node config service request
+        :param request: get node service request
         :param context: grpc context
-        :return: get node config service response
+        :return: get node service response
         """
         session = self.get_session(request.session_id, context)
         node = self.get_node(session, request.node_id, context, CoreNode)
         self.validate_service(request.name, context)
-        service = node.config_services.get(request.name)
+        service = node.services.get(request.name)
         if service:
             config = service.render_config()
         else:
@@ -1121,16 +1123,16 @@ def GetServiceRendered(
         self, request: GetServiceRenderedRequest, context: ServicerContext
     ) -> GetServiceRenderedResponse:
         """
-        Retrieves the rendered file data for a given config service on a node.
+        Retrieves the rendered file data for a given service on a node.
 
-        :param request: config service render request
+        :param request: service render request
         :param context: grpc context
-        :return: rendered config service files
+        :return: rendered service files
         """
         session = self.get_session(request.session_id, context)
         node = self.get_node(session, request.node_id, context, CoreNode)
         self.validate_service(request.name, context)
-        service = node.config_services.get(request.name)
+        service = node.services.get(request.name)
         if not service:
             context.abort(
                 grpc.StatusCode.NOT_FOUND, f"unknown node service {request.name}"
@@ -1142,11 +1144,11 @@ def GetServiceDefaults(
         self, request: GetServiceDefaultsRequest, context: ServicerContext
     ) -> GetServiceDefaultsResponse:
         """
-        Get default values for a given configuration service.
+        Get default values for a given service.
 
-        :param request: get config service defaults request
+        :param request: get service defaults request
         :param context: grpc context
-        :return: get config service defaults response
+        :return: get service defaults response
         """
         session = self.get_session(request.session_id, context)
         node = self.get_node(session, request.node_id, context, CoreNode)
diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py
index 569f76bc3..3b31089c4 100644
--- a/daemon/core/emulator/coreemu.py
+++ b/daemon/core/emulator/coreemu.py
@@ -63,10 +63,10 @@ def _load_services(self) -> None:
 
         :return: nothing
         """
-        # load default config services
+        # load default services
         self.service_manager.load_locals()
-        # load custom config services
-        custom_dir = self.config.get("custom_config_services_dir")
+        # load custom services
+        custom_dir = self.config.get("custom_services_dir")
         if custom_dir is not None:
             custom_dir = Path(custom_dir)
             self.service_manager.load(custom_dir)
diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py
index 3af22dd7f..5cc6d75ed 100644
--- a/daemon/core/emulator/data.py
+++ b/daemon/core/emulator/data.py
@@ -64,7 +64,6 @@ class NodeOptions:
     canvas: int = None
     icon: str = None
     services: list[str] = field(default_factory=list)
-    config_services: list[str] = field(default_factory=list)
     x: float = None
     y: float = None
     lat: float = None
@@ -73,7 +72,6 @@ class NodeOptions:
     server: str = None
     image: str = None
     emane: str = None
-    legacy: bool = False
     # src, dst
     binds: list[tuple[str, str]] = field(default_factory=list)
     # src, dst, unique, delete
diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py
index 43cd8c541..b8772ada1 100644
--- a/daemon/core/emulator/session.py
+++ b/daemon/core/emulator/session.py
@@ -154,7 +154,7 @@ def __init__(
         self.emane: EmaneManager = EmaneManager(self)
         self.sdt: Sdt = Sdt(self)
 
-        # config services
+        # services
         self.service_manager: Optional[ServiceManager] = None
 
     @classmethod
@@ -1052,7 +1052,7 @@ def data_collect(self) -> None:
             funcs = []
             for node in self.nodes.values():
                 if isinstance(node, CoreNodeBase) and node.up:
-                    funcs.append((node.stop_config_services, (), {}))
+                    funcs.append((node.stop_services, (), {}))
             utils.threadpool(funcs)
 
         # shutdown emane
@@ -1084,11 +1084,11 @@ def boot_node(self, node: CoreNode) -> None:
         :return: nothing
         """
         logger.info(
-            "booting node(%s): config services(%s)",
+            "booting node(%s): services(%s)",
             node.name,
-            ", ".join(node.config_services.keys()),
+            ", ".join(node.services.keys()),
         )
-        node.start_config_services()
+        node.start_services()
 
     def boot_nodes(self) -> list[Exception]:
         """
diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py
index 20c6123e8..44778dfb2 100644
--- a/daemon/core/gui/coreclient.py
+++ b/daemon/core/gui/coreclient.py
@@ -73,8 +73,8 @@ def __init__(self, app: "Application", proxy: bool) -> None:
         self.show_throughputs: tk.BooleanVar = tk.BooleanVar(value=False)
 
         # global service settings
-        self.config_services_groups: dict[str, set[str]] = {}
-        self.config_services: dict[str, Service] = {}
+        self.services_groups: dict[str, set[str]] = {}
+        self.services: dict[str, Service] = {}
 
         # loaded configuration data
         self.emane_models: list[str] = []
@@ -352,14 +352,12 @@ def setup(self, session_id: int = None) -> None:
         """
         try:
             self.client.connect()
-            # get current core configurations services/config services
+            # get current core configurations
             core_config = self.client.get_config()
             self.emane_models = sorted(core_config.emane_models)
             for service in core_config.services:
-                self.config_services[service.name] = service
-                group_services = self.config_services_groups.setdefault(
-                    service.group, set()
-                )
+                self.services[service.name] = service
+                group_services = self.services_groups.setdefault(service.group, set())
                 group_services.add(service.name)
             # join provided session, create new session, or show dialog to select an
             # existing session
@@ -685,11 +683,11 @@ def get_emane_model_configs(self) -> list[EmaneModelConfig]:
                 configs.append(config)
         return configs
 
-    def get_config_service_rendered(self, node_id: int, name: str) -> dict[str, str]:
-        return self.client.get_config_service_rendered(self.session.id, node_id, name)
+    def get_service_rendered(self, node_id: int, name: str) -> dict[str, str]:
+        return self.client.get_service_rendered(self.session.id, node_id, name)
 
-    def get_config_service_defaults(self, node_id: int, name: str) -> ServiceDefaults:
-        return self.client.get_config_service_defaults(self.session.id, node_id, name)
+    def get_service_defaults(self, node_id: int, name: str) -> ServiceDefaults:
+        return self.client.get_service_defaults(self.session.id, node_id, name)
 
     def run(self, node_id: int) -> str:
         logger.info("running node(%s) cmd: %s", node_id, self.observer)
diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py
index ea4421e89..e891458f9 100644
--- a/daemon/core/gui/dialogs/customnodes.py
+++ b/daemon/core/gui/dialogs/customnodes.py
@@ -23,7 +23,7 @@ class ServicesSelectDialog(Dialog):
     def __init__(
         self, master: tk.BaseWidget, app: "Application", current_services: set[str]
     ) -> None:
-        super().__init__(app, "Node Config Services", master=master)
+        super().__init__(app, "Node Services", master=master)
         self.groups: Optional[ListboxScroll] = None
         self.services: Optional[CheckboxList] = None
         self.current: Optional[ListboxScroll] = None
@@ -45,7 +45,7 @@ def draw(self) -> None:
         label_frame.columnconfigure(0, weight=1)
         self.groups = ListboxScroll(label_frame)
         self.groups.grid(sticky=tk.NSEW)
-        for group in sorted(self.app.core.config_services_groups):
+        for group in sorted(self.app.core.services_groups):
             self.groups.listbox.insert(tk.END, group)
         self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
         self.groups.listbox.selection_set(0)
@@ -86,7 +86,7 @@ def handle_group_change(self, event: tk.Event = None) -> None:
             index = selection[0]
             group = self.groups.listbox.get(index)
             self.services.clear()
-            for name in sorted(self.app.core.config_services_groups[group]):
+            for name in sorted(self.app.core.services_groups[group]):
                 checked = name in self.current_services
                 self.services.add(name, checked)
 
@@ -147,7 +147,7 @@ def draw_node_config(self) -> None:
             frame, text="Icon", compound=tk.LEFT, command=self.click_icon
         )
         self.image_button.grid(sticky=tk.EW, pady=PADY)
-        button = ttk.Button(frame, text="Config Services", command=self.click_services)
+        button = ttk.Button(frame, text="Services", command=self.click_services)
         button.grid(sticky=tk.EW)
 
     def draw_node_buttons(self) -> None:
diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeservice.py
similarity index 92%
rename from daemon/core/gui/dialogs/nodeconfigservice.py
rename to daemon/core/gui/dialogs/nodeservice.py
index 44b1c4359..04fed934e 100644
--- a/daemon/core/gui/dialogs/nodeconfigservice.py
+++ b/daemon/core/gui/dialogs/nodeservice.py
@@ -7,8 +7,8 @@
 from typing import TYPE_CHECKING, Optional
 
 from core.api.grpc.wrappers import Node
-from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
 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
 
@@ -18,11 +18,11 @@
     from core.gui.app import Application
 
 
-class NodeConfigServiceDialog(Dialog):
+class NodeServiceDialog(Dialog):
     def __init__(
         self, app: "Application", node: Node, services: set[str] = None
     ) -> None:
-        title = f"{node.name} Config Services"
+        title = f"{node.name} Services"
         super().__init__(app, title)
         self.node: Node = node
         self.groups: Optional[ListboxScroll] = None
@@ -49,7 +49,7 @@ def draw(self) -> None:
         label_frame.columnconfigure(0, weight=1)
         self.groups = ListboxScroll(label_frame)
         self.groups.grid(sticky=tk.NSEW)
-        for group in sorted(self.app.core.config_services_groups):
+        for group in sorted(self.app.core.services_groups):
             self.groups.listbox.insert(tk.END, group)
         self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
         self.groups.listbox.selection_set(0)
@@ -94,7 +94,7 @@ def handle_group_change(self, event: tk.Event = None) -> None:
             index = selection[0]
             group = self.groups.listbox.get(index)
             self.services.clear()
-            for name in sorted(self.app.core.config_services_groups[group]):
+            for name in sorted(self.app.core.services_groups[group]):
                 checked = name in self.current_services
                 self.services.add(name, checked)
 
@@ -110,7 +110,7 @@ def service_clicked(self, name: str, var: tk.IntVar) -> None:
     def click_configure(self) -> None:
         current_selection = self.current.listbox.curselection()
         if len(current_selection):
-            dialog = ConfigServiceConfigDialog(
+            dialog = ServiceConfigDialog(
                 self,
                 self.app,
                 self.current.listbox.get(current_selection[0]),
@@ -121,7 +121,7 @@ def click_configure(self) -> None:
                 self.draw_current_services()
         else:
             messagebox.showinfo(
-                "Config Service Configuration",
+                "Service Configuration",
                 "Select a service to configure",
                 parent=self,
             )
@@ -135,7 +135,7 @@ def draw_current_services(self) -> None:
 
     def click_save(self) -> None:
         self.node.services = self.current_services.copy()
-        logger.info("saved node config services: %s", self.node.services)
+        logger.info("saved node services: %s", self.node.services)
         self.destroy()
 
     def click_cancel(self) -> None:
diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py
similarity index 96%
rename from daemon/core/gui/dialogs/configserviceconfig.py
rename to daemon/core/gui/dialogs/serviceconfig.py
index a3d19e578..a48536ec9 100644
--- a/daemon/core/gui/dialogs/configserviceconfig.py
+++ b/daemon/core/gui/dialogs/serviceconfig.py
@@ -25,11 +25,11 @@
     from core.gui.coreclient import CoreClient
 
 
-class ConfigServiceConfigDialog(Dialog):
+class ServiceConfigDialog(Dialog):
     def __init__(
         self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node
     ) -> None:
-        title = f"{service_name} Config Service"
+        title = f"{service_name} Service"
         super().__init__(app, title, master=master)
         self.core: "CoreClient" = app.core
         self.node: Node = node
@@ -76,7 +76,7 @@ def __init__(
     def load(self) -> None:
         try:
             self.core.start_session(definition=True)
-            service = self.core.config_services[self.service_name]
+            service = self.core.services[self.service_name]
             self.dependencies = service.dependencies[:]
             self.executables = service.executables[:]
             self.directories = service.directories[:]
@@ -87,16 +87,14 @@ def load(self) -> None:
             self.validation_mode = service.validation_mode
             self.validation_time = service.validation_timer
             self.validation_period.set(service.validation_period)
-            defaults = self.core.get_config_service_defaults(
-                self.node.id, self.service_name
-            )
+            defaults = self.core.get_service_defaults(self.node.id, self.service_name)
             self.original_service_files = defaults.templates
             self.temp_service_files = dict(self.original_service_files)
             self.modes = sorted(defaults.modes)
             self.mode_configs = defaults.modes
             self.config = ConfigOption.from_dict(defaults.config)
             self.default_config = {x.name: x.value for x in self.config.values()}
-            self.rendered = self.core.get_config_service_rendered(
+            self.rendered = self.core.get_service_rendered(
                 self.node.id, self.service_name
             )
             service_config = self.node.service_configs.get(self.service_name)
@@ -108,7 +106,7 @@ def load(self) -> None:
                     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.app.show_grpc_exception("Get Service Error", e)
             self.has_error = True
 
     def draw(self) -> None:
@@ -208,7 +206,7 @@ def draw_tab_config(self) -> None:
             self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
             self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY)
 
-        logger.info("config service config: %s", self.config)
+        logger.info("service config: %s", self.config)
         self.config_frame = ConfigFrame(tab, self.app, self.config)
         self.config_frame.draw_config()
         self.config_frame.grid(sticky=tk.NSEW, pady=PADY)
@@ -385,10 +383,8 @@ def click_defaults(self) -> None:
         self.temp_service_files = dict(self.original_service_files)
         # reset session definition and retrieve default rendered templates
         self.core.start_session(definition=True)
-        self.rendered = self.core.get_config_service_rendered(
-            self.node.id, self.service_name
-        )
-        logger.info("cleared config service config: %s", self.node.service_configs)
+        self.rendered = self.core.get_service_rendered(self.node.id, self.service_name)
+        logger.info("cleared service config: %s", self.node.service_configs)
         # reset current selected file data and config data, if present
         template_name = self.templates_combobox.get()
         temp_data = self.temp_service_files[template_name]
diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py
index 9437800d4..c030ccda7 100644
--- a/daemon/core/gui/graph/node.py
+++ b/daemon/core/gui/graph/node.py
@@ -14,7 +14,7 @@
 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.wirelessconfig import WirelessConfigDialog
 from core.gui.dialogs.wlanconfig import WlanConfigDialog
 from core.gui.frames.node import NodeInfoFrame
@@ -259,9 +259,7 @@ def show_context(self, event: tk.Event) -> None:
         else:
             self.context.add_command(label="Configure", command=self.show_config)
             if nutils.is_container(self.core_node):
-                self.context.add_command(
-                    label="Config Services", command=self.show_config_services
-                )
+                self.context.add_command(label="Services", command=self.show_services)
             if is_emane:
                 self.context.add_command(
                     label="EMANE Config", command=self.show_emane_config
@@ -374,8 +372,8 @@ def show_emane_config(self) -> None:
         dialog = EmaneConfigDialog(self.app, self.core_node)
         dialog.show()
 
-    def show_config_services(self) -> None:
-        dialog = NodeConfigServiceDialog(self.app, self.core_node)
+    def show_services(self) -> None:
+        dialog = NodeServiceDialog(self.app, self.core_node)
         dialog.show()
 
     def has_emane_link(self, iface_id: int) -> Node:
@@ -471,7 +469,7 @@ def set_label(self, state: str) -> None:
     def _service_action(self, service: str, action: ServiceAction) -> None:
         session_id = self.app.core.session.id
         try:
-            result = self.app.core.client.config_service_action(
+            result = self.app.core.client.service_action(
                 session_id, self.core_node.id, service, action
             )
             if not result:
diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py
index 754df3be5..0f048a2b3 100644
--- a/daemon/core/nodes/base.py
+++ b/daemon/core/nodes/base.py
@@ -28,7 +28,7 @@
     from core.emulator.session import Session
     from core.services.base import Service
 
-    ConfigServiceType = type[Service]
+    ServiceType = type[Service]
 
 PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")]
 
@@ -113,12 +113,8 @@ class CoreNodeOptions(NodeOptions):
     """model is used for providing a default set of services"""
     services: list[str] = field(default_factory=list)
     """services to start within node"""
-    config_services: list[str] = field(default_factory=list)
-    """config services to start within node"""
     directory: Path = None
     """directory to define node, defaults to path under the session directory"""
-    legacy: bool = False
-    """legacy nodes default to standard services"""
 
 
 class NodeBase(abc.ABC):
@@ -385,14 +381,14 @@ def __init__(
         """
         Create a CoreNodeBase instance.
 
-        :param session: CORE session object
-        :param _id: object id
-        :param name: object name
+        :param session: session owning this node
+        :param _id: id of this node
+        :param name: name of this node
         :param server: remote server node
             will run on, default is None for localhost
         """
         super().__init__(session, _id, name, server, options)
-        self.config_services: dict[str, "Service"] = {}
+        self.services: dict[str, "Service"] = {}
         self.directory: Optional[Path] = None
         self.tmpnodedir: bool = False
 
@@ -466,17 +462,17 @@ def host_path(self, path: Path, is_dir: bool = False) -> Path:
             directory = str(path.parent).strip("/").replace("/", ".")
             return self.directory / directory / path.name
 
-    def add_config_service(self, service_class: "ConfigServiceType") -> None:
+    def add_service(self, service_class: "ServiceType") -> None:
         """
-        Adds a configuration service to the node.
+        Adds a service to the node.
 
-        :param service_class: configuration service class to assign to node
+        :param service_class: service class to assign to node
         :return: nothing
         """
         name = service_class.name
-        if name in self.config_services:
+        if name in self.services:
             raise CoreError(f"node({self.name}) already has service({name})")
-        self.config_services[name] = service_class(self)
+        self.services[name] = service_class(self)
 
     def set_service_config(self, name: str, data: dict[str, str]) -> None:
         """
@@ -486,30 +482,30 @@ def set_service_config(self, name: str, data: dict[str, str]) -> None:
         :param data: custom config data to set
         :return: nothing
         """
-        service = self.config_services.get(name)
+        service = self.services.get(name)
         if service is None:
             raise CoreError(f"node({self.name}) does not have service({name})")
         service.set_config(data)
 
-    def start_config_services(self) -> None:
+    def start_services(self) -> None:
         """
-        Determines startup paths and starts configuration services, based on their
+        Determines startup paths and starts services, based on their
         dependency chains.
 
         :return: nothing
         """
-        startup_paths = ServiceDependencies(self.config_services).startup_paths()
+        startup_paths = ServiceDependencies(self.services).startup_paths()
         for startup_path in startup_paths:
             for service in startup_path:
                 service.start()
 
-    def stop_config_services(self) -> None:
+    def stop_services(self) -> None:
         """
-        Stop all configuration services.
+        Stop all services.
 
         :return: nothing
         """
-        for service in self.config_services.values():
+        for service in self.services.values():
             service.stop()
 
     def makenodedir(self) -> None:
@@ -586,19 +582,19 @@ def __init__(
         )
         options = options or CoreNodeOptions()
         self.model: Optional[str] = options.model
-        # add config services
-        config_services = options.config_services
-        if not config_services:
-            config_services = self.session.service_manager.defaults.get(self.model, [])
+        # add services
+        services = options.services
+        if not services:
+            services = self.session.service_manager.defaults.get(self.model, [])
         logger.info(
-            "setting node(%s) model(%s) config services: %s",
+            "setting node(%s) model(%s) services: %s",
             self.name,
             self.model,
-            config_services,
+            services,
         )
-        for name in config_services:
+        for name in services:
             service_class = self.session.service_manager.get_service(name)
-            self.add_config_service(service_class)
+            self.add_service(service_class)
 
     @classmethod
     def create_options(cls) -> CoreNodeOptions:
diff --git a/daemon/core/services/defaults/frrservices/services.py b/daemon/core/services/defaults/frrservices/services.py
index 7c331ecf3..e71eea62a 100644
--- a/daemon/core/services/defaults/frrservices/services.py
+++ b/daemon/core/services/defaults/frrservices/services.py
@@ -111,7 +111,7 @@ def data(self) -> dict[str, Any]:
         services = []
         want_ip4 = False
         want_ip6 = False
-        for service in self.node.config_services.values():
+        for service in self.node.services.values():
             if self.name not in service.dependencies:
                 continue
             if not isinstance(service, FrrService):
diff --git a/daemon/core/services/defaults/nrlservices/services.py b/daemon/core/services/defaults/nrlservices/services.py
index 7ea5e395f..8815c89ba 100644
--- a/daemon/core/services/defaults/nrlservices/services.py
+++ b/daemon/core/services/defaults/nrlservices/services.py
@@ -44,7 +44,7 @@ class NrlNhdp(Service):
     modes: dict[str, dict[str, str]] = {}
 
     def data(self) -> dict[str, Any]:
-        has_smf = "SMF" in self.node.config_services
+        has_smf = "SMF" in self.node.services
         ifnames = []
         for iface in self.node.get_ifaces(control=False):
             ifnames.append(iface.name)
@@ -66,8 +66,8 @@ class NrlSmf(Service):
     modes: dict[str, dict[str, str]] = {}
 
     def data(self) -> dict[str, Any]:
-        has_nhdp = "NHDP" in self.node.config_services
-        has_olsr = "OLSR" in self.node.config_services
+        has_nhdp = "NHDP" in self.node.services
+        has_olsr = "OLSR" in self.node.services
         ifnames = []
         ip4_prefix = None
         for iface in self.node.get_ifaces(control=False):
@@ -96,8 +96,8 @@ class NrlOlsr(Service):
     modes: dict[str, dict[str, str]] = {}
 
     def data(self) -> dict[str, Any]:
-        has_smf = "SMF" in self.node.config_services
-        has_zebra = "zebra" in self.node.config_services
+        has_smf = "SMF" in self.node.services
+        has_zebra = "zebra" in self.node.services
         ifname = None
         for iface in self.node.get_ifaces(control=False):
             ifname = iface.name
@@ -120,7 +120,7 @@ class NrlOlsrv2(Service):
     modes: dict[str, dict[str, str]] = {}
 
     def data(self) -> dict[str, Any]:
-        has_smf = "SMF" in self.node.config_services
+        has_smf = "SMF" in self.node.services
         ifnames = []
         for iface in self.node.get_ifaces(control=False):
             ifnames.append(iface.name)
@@ -142,7 +142,7 @@ class OlsrOrg(Service):
     modes: dict[str, dict[str, str]] = {}
 
     def data(self) -> dict[str, Any]:
-        has_smf = "SMF" in self.node.config_services
+        has_smf = "SMF" in self.node.services
         ifnames = []
         for iface in self.node.get_ifaces(control=False):
             ifnames.append(iface.name)
diff --git a/daemon/core/services/defaults/quaggaservices/services.py b/daemon/core/services/defaults/quaggaservices/services.py
index 634b7cd80..3de0dbf46 100644
--- a/daemon/core/services/defaults/quaggaservices/services.py
+++ b/daemon/core/services/defaults/quaggaservices/services.py
@@ -112,7 +112,7 @@ def data(self) -> dict[str, Any]:
         services = []
         want_ip4 = False
         want_ip6 = False
-        for service in self.node.config_services.values():
+        for service in self.node.services.values():
             if self.name not in service.dependencies:
                 continue
             if not isinstance(service, QuaggaService):
diff --git a/daemon/core/services/dependencies.py b/daemon/core/services/dependencies.py
index 492718a7f..0b74a4266 100644
--- a/daemon/core/services/dependencies.py
+++ b/daemon/core/services/dependencies.py
@@ -75,7 +75,7 @@ def _start(self, service: "Service") -> list["Service"]:
         Starts a path for checking dependencies for a given service.
 
         :param service: service to check dependencies for
-        :return: list of config services to start in order
+        :return: list of services to start in order
         """
         logger.debug("starting service dependency check: %s", service.name)
         self._reset()
diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py
index cfea8d696..35aa65c1e 100644
--- a/daemon/core/xml/corexml.py
+++ b/daemon/core/xml/corexml.py
@@ -174,11 +174,11 @@ def add_services(self) -> None:
         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)
+        service_elements = etree.Element("configservices")
+        for name, service in self.node.services.items():
+            etree.SubElement(service_elements, "service", name=name)
+        if service_elements.getchildren():
+            self.element.append(service_elements)
 
 
 class NetworkElement(NodeElement):
@@ -225,7 +225,7 @@ def write_session(self) -> None:
         self.write_links()
         self.write_mobility_configs()
         self.write_emane_configs()
-        self.write_configservice_configs()
+        self.write_service_configs()
         self.write_session_origin()
         self.write_servers()
         self.write_session_hooks()
@@ -347,12 +347,12 @@ def write_mobility_configs(self) -> None:
         if mobility_configurations.getchildren():
             self.scenario.append(mobility_configurations)
 
-    def write_configservice_configs(self) -> None:
+    def write_service_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():
+            for name, service in node.services.items():
                 service_element = etree.SubElement(
                     service_configurations, "service", name=name
                 )
@@ -511,7 +511,7 @@ def read(self, file_path: Path) -> None:
         self.read_nodes()
         self.read_links()
         self.read_emane_configs()
-        self.read_configservice_configs()
+        self.read_service_configs()
 
     def read_default_services(self) -> None:
         default_services = self.scenario.find("default_services")
@@ -699,16 +699,11 @@ def read_device(self, device_element: etree.Element) -> None:
         # check for special options
         if isinstance(options, CoreNodeOptions):
             options.model = model
-            service_elements = device_element.find("services")
+            service_elements = device_element.find("configservices")
             if service_elements is not None:
                 options.services.extend(
                     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.extend(
-                    x.get("name") for x in config_service_elements.iterchildren()
-                )
         if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)):
             options.image = image
         # get position information
@@ -766,18 +761,18 @@ def read_network(self, network_element: etree.Element) -> None:
                     config[name] = value
                 node.set_config(config)
 
-    def read_configservice_configs(self) -> None:
-        configservice_configs = self.scenario.find("configservice_configurations")
-        if configservice_configs is None:
+    def read_service_configs(self) -> None:
+        service_configs = self.scenario.find("configservice_configurations")
+        if service_configs is None:
             return
 
-        for configservice_element in configservice_configs.iterchildren():
-            name = configservice_element.get("name")
-            node_id = get_int(configservice_element, "node")
+        for service_element in service_configs.iterchildren():
+            name = service_element.get("name")
+            node_id = get_int(service_element, "node")
             node = self.session.get_node(node_id, CoreNodeBase)
-            service = node.config_services[name]
+            service = node.services[name]
 
-            configs_element = configservice_element.find("configs")
+            configs_element = service_element.find("configs")
             if configs_element is not None:
                 config = {}
                 for config_element in configs_element.iterchildren():
@@ -786,7 +781,7 @@ def read_configservice_configs(self) -> None:
                     config[key] = value
                 service.set_config(config)
 
-            templates_element = configservice_element.find("templates")
+            templates_element = service_element.find("templates")
             if templates_element is not None:
                 for template_element in templates_element.iterchildren():
                     name = template_element.get("name")
diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml
index 0d1acf7aa..2cde6020b 100644
--- a/daemon/pyproject.toml
+++ b/daemon/pyproject.toml
@@ -8,7 +8,7 @@ repository = "https://github.com/coreemu/core"
 documentation = "https://coreemu.github.io/core/"
 include = [
     "core/api/grpc/*",
-    "core/configservices/*/templates",
+    "core/services/defaults/*/templates",
     "core/constants.py",
     "core/gui/data/**/*",
 ]
diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py
index a878b3308..ef5bcaf2a 100644
--- a/daemon/tests/test_grpc.py
+++ b/daemon/tests/test_grpc.py
@@ -142,7 +142,7 @@ def test_start_session(self, grpc_server: CoreGrpcServer, definition):
         )
         assert set_mobility_config[mobility_config_key] == mobility_config_value
         real_node1 = real_session.get_node(node1.id, CoreNode)
-        real_service = real_node1.config_services[service_name]
+        real_service = real_node1.services[service_name]
         real_templates = real_service.get_templates()
         real_template_data = real_templates[file_name]
         assert file_data == real_template_data
@@ -611,7 +611,7 @@ def test_mobility_action(self, grpc_server: CoreGrpcServer):
         # then
         assert result is True
 
-    def test_config_service_action(self, grpc_server: CoreGrpcServer):
+    def test_service_action(self, grpc_server: CoreGrpcServer):
         # given
         client = CoreGrpcClient()
         session = grpc_server.coreemu.create_session()
@@ -620,7 +620,7 @@ def test_config_service_action(self, grpc_server: CoreGrpcServer):
 
         # then
         with client.context_connect():
-            result = client.config_service_action(
+            result = client.service_action(
                 session.id, node.id, service_name, ServiceAction.STOP
             )
 
diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py
index 9470a42f6..01e5f6eb5 100644
--- a/daemon/tests/test_xml.py
+++ b/daemon/tests/test_xml.py
@@ -125,7 +125,7 @@ def test_xml_ptp_services(
         session.add_link(node1.id, node2.id, iface1_data, iface2_data)
 
         # set custom values for node service
-        service = node1.config_services[DefaultRouteService.name]
+        service = node1.services[DefaultRouteService.name]
         file_name = DefaultRouteService.files[0]
         file_data = "# test"
         service.set_template(file_name, file_data)
@@ -156,7 +156,7 @@ def test_xml_ptp_services(
 
         # retrieve custom service
         node1_xml = session.get_node(node1.id, CoreNode)
-        service_xml = node1_xml.config_services[DefaultRouteService.name]
+        service_xml = node1_xml.services[DefaultRouteService.name]
 
         # verify nodes have been recreated
         assert session.get_node(node1.id, CoreNode)
diff --git a/docs/configservices.md b/docs/configservices.md
deleted file mode 100644
index a2564642a..000000000
--- a/docs/configservices.md
+++ /dev/null
@@ -1,196 +0,0 @@
-# Config Services
-
-## Overview
-
-Config services are a newer version of services for CORE, that leverage a
-templating engine, for more robust service file creation. They also
-have the power of configuration key/value pairs that values that can be
-defined and displayed within the GUI, to help further tweak a service,
-as needed.
-
-CORE services are a convenience for creating reusable dynamic scripts
-to run on nodes, for carrying out specific task(s).
-
-This boilds down to the following functions:
-
-* generating files the service will use, either directly for commands or for configuration
-* command(s) for starting a service
-* command(s) for validating a service
-* command(s) for stopping a service
-
-Most CORE nodes will have a default set of services to run, associated with
-them. You can however customize the set of services a node will use. Or even
-further define a new node type within the GUI, with a set of services, that
-will allow quickly dragging and dropping that node type during creation.
-
-## Available Services
-
-| Service Group                    | Services                                                              |
-|----------------------------------|-----------------------------------------------------------------------|
-| [BIRD](services/bird.md)         | BGP, OSPF, RADV, RIP, Static                                          |
-| [EMANE](services/emane.md)       | Transport Service                                                     |
-| [FRR](services/frr.md)           | BABEL, BGP, OSPFv2, OSPFv3, PIMD, RIP, RIPNG, Zebra                   |
-| [NRL](services/nrl.md)           | arouted, MGEN Sink, MGEN Actor, NHDP, OLSR, OLSRORG, OLSRv2, SMF      |
-| [Quagga](services/quagga.md)     | BABEL, BGP, OSPFv2, OSPFv3, OSPFv3 MDR, RIP, RIPNG, XPIMD, Zebra      |
-| [SDN](services/sdn.md)           | OVS, RYU                                                              |
-| [Security](services/security.md) | Firewall, IPsec, NAT, VPN Client, VPN Server                          |
-| [Utility](services/utility.md)   | ATD, Routing Utils, DHCP, FTP, IP Forward, PCAP, RADVD, SSF, UCARP    |
-| [XORP](services/xorp.md)         | BGP, OLSR, OSPFv2, OSPFv3, PIMSM4, PIMSM6, RIP, RIPNG, Router Manager |
-
-## Node Types and Default Services
-
-Here are the default node types and their services:
-
-| Node Type | Services                                                                                                                                   |
-|-----------|--------------------------------------------------------------------------------------------------------------------------------------------|
-| *router*  | zebra, OSFPv2, OSPFv3, and IPForward services for IGP link-state routing.                                                                  |
-| *PC*      | DefaultRoute service for having a default route when connected directly to a router.                                                       |
-| *mdr*     | zebra, OSPFv3MDR, and IPForward services for wireless-optimized MANET Designated Router routing.                                           |
-| *prouter* | a physical router, having the same default services as the *router* node type; for incorporating Linux testbed machines into an emulation. |
-
-Configuration files can be automatically generated by each service. For
-example, CORE automatically generates routing protocol configuration for the
-router nodes in order to simplify the creation of virtual networks.
-
-To change the services associated with a node, double-click on the node to
-invoke its configuration dialog and click on the *Services...* button,
-or right-click a node a choose *Services...* from the menu.
-Services are enabled or disabled by clicking on their names. The button next to
-each service name allows you to customize all aspects of this service for this
-node. For example, special route redistribution commands could be inserted in
-to the Quagga routing configuration associated with the zebra service.
-
-To change the default services associated with a node type, use the Node Types
-dialog available from the *Edit* button at the end of the Layer-3 nodes
-toolbar, or choose *Node types...* from the  *Session* menu. Note that
-any new services selected are not applied to existing nodes if the nodes have
-been customized.
-
-The node types are saved in the GUI config file **~/.coregui/config.yaml**.
-Keep this in mind when changing the default services for
-existing node types; it may be better to simply create a new node type. It is
-recommended that you do not change the default built-in node types.
-
-## New Services
-
-Services can save time required to configure nodes, especially if a number
-of nodes require similar configuration procedures. New services can be
-introduced to automate tasks.
-
-### Creating New Services
-
-!!! note
-
-    The directory base name used in **custom_services_dir** below should
-    be unique and should not correspond to any existing Python module name.
-    For example, don't use the name **subprocess** or **services**.
-
-1. Modify the example service shown below
-   to do what you want. It could generate config/script files, mount per-node
-   directories, start processes/scripts, etc. Your file can define one or more
-   classes to be imported. You can create multiple Python files that will be imported.
-
-2. Put these files in a directory such as **~/.coregui/custom_services**.
-
-3. Add a **custom_config_services_dir = ~/.coregui/custom_services** entry to the
-   /opt/core/etc/core.conf file.
-
-4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax)
-   should be displayed in the terminal (or service log, like journalctl).
-
-5. Start using your custom service on your nodes. You can create a new node
-   type that uses your service, or change the default services for an existing
-   node type, or change individual nodes.
-
-### Example Custom Service
-
-Below is the skeleton for a custom service with some documentation. Most
-people would likely only setup the required class variables **(name/group)**.
-Then define the **files** to generate and implement the
-**get_text_template** function to dynamically create the files wanted. Finally,
-the **startup** commands would be supplied, which typically tend to be
-running the shell files generated.
-
-```python
-from typing import Dict, List
-
-from core.config import ConfigString, ConfigBool, Configuration
-from core.services.base import Service, ServiceMode, ShadowDir
-
-
-# class that subclasses ConfigService
-class ExampleService(Service):
-    # unique name for your service within CORE
-    name: str = "Example"
-    # the group your service is associated with, used for display in GUI
-    group: str = "ExampleGroup"
-    # directories that the service should shadow mount, hiding the system directory
-    directories: List[str] = [
-        "/usr/local/core",
-    ]
-    # files that this service should generate, defaults to nodes home directory
-    # or can provide an absolute path to a mounted directory
-    files: List[str] = [
-        "example-start.sh",
-        "/usr/local/core/file1",
-    ]
-    # executables that should exist on path, that this service depends on
-    executables: List[str] = []
-    # other services that this service depends on, can be used to define service start order
-    dependencies: List[str] = []
-    # commands to run to start this service
-    startup: List[str] = []
-    # commands to run to validate this service
-    validate: List[str] = []
-    # commands to run to stop this service
-    shutdown: List[str] = []
-    # validation mode, blocking, non-blocking, and timer
-    validation_mode: ServiceMode = ServiceMode.BLOCKING
-    # configurable values that this service can use, for file generation
-    default_configs: List[Configuration] = [
-        ConfigString(id="value1", label="Text"),
-        ConfigBool(id="value2", label="Boolean"),
-        ConfigString(id="value3", label="Multiple Choice", options=["value1", "value2", "value3"]),
-    ]
-    # sets of values to set for the configuration defined above, can be used to
-    # provide convenient sets of values to typically use
-    modes: Dict[str, Dict[str, str]] = {
-        "mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
-        "mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
-        "mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
-    }
-    # defines directories that this service can help shadow within a node
-    shadow_directories: List[ShadowDir] = [
-        ShadowDir(path="/user/local/core", src="/opt/core")
-    ]
-
-    def get_text_template(self, name: str) -> str:
-        return """
-        # sample script 1
-        # node id(${node.id}) name(${node.name})
-        # config: ${config}
-        echo hello
-        """
-```
-
-#### Validation Mode
-
-Validation modes are used to determine if a service has started up successfully.
-
-* blocking - startup commands are expected to run til completion and return 0 exit code
-* non-blocking - startup commands are ran, but do not wait for completion
-* timer - startup commands are ran, and an arbitrary amount of time is waited to consider started
-
-#### Shadow Directories
-
-Shadow directories provide a convenience for copying a directory and the files within
-it to a nodes home directory, to allow a unique set of per node files.
-
-* `ShadowDir(path="/user/local/core")` - copies files at the given location into the node
-* `ShadowDir(path="/user/local/core", src="/opt/core")` - copies files to the given location,
-  but sourced from the provided location
-* `ShadowDir(path="/user/local/core", templates=True)` - copies files and treats them as
-  templates for generation
-* `ShadowDir(path="/user/local/core", has_node_paths=True)` - copies files from the given
-  location, and looks for unique node names directories within it, using a directory named
-  default, when not preset
diff --git a/docs/services.md b/docs/services.md
index 32f8c8982..fa410f840 100644
--- a/docs/services.md
+++ b/docs/services.md
@@ -1,22 +1,26 @@
-# Services (Deprecated)
+# Services
 
 ## Overview
 
-CORE uses the concept of services to specify what processes or scripts run on a
-node when it is started. Layer-3 nodes such as routers and PCs are defined by
-the services that they run.
+CORE uses the concept of services to specify what processes or scripts to run on a
+node when it is started. Ultimately, providing a convenience for creating reusable
+dynamic scripts to run on nodes, for carrying out specific tasks.
 
-Services may be customized for each node, or new custom services can be
-created. New node types can be created each having a different name, icon, and
-set of default services. Each service defines the per-node directories,
-configuration files, startup index, starting commands, validation commands,
-shutdown commands, and meta-data associated with a node.
+Services leverage a templating engine, for robust service file creation.
+They also have the power of configuration key/value pairs, that can be
+defined and displayed within the GUI, to help further configure a service, as needed.
 
-!!! note
+This boils down to the following functions:
+
+* generating files the service will use, either directly for commands or for configuration
+* command(s) for starting a service
+* command(s) for validating a service
+* command(s) for stopping a service
 
-    **Network namespace nodes do not undergo the normal Linux boot process**
-    using the **init**, **upstart**, or **systemd** frameworks. These
-    lightweight nodes use configured CORE *services*.
+Most CORE nodes will have a default set of services to run, associated with
+them. You can however customize the set of services a node will use. Or even
+further define a new node type within the GUI, with a set of services, that
+will allow quickly dragging and dropping that node type during creation.
 
 ## Available Services
 
@@ -39,7 +43,6 @@ Here are the default node types and their services:
 | Node Type | Services                                                                                                                                   |
 |-----------|--------------------------------------------------------------------------------------------------------------------------------------------|
 | *router*  | zebra, OSFPv2, OSPFv3, and IPForward services for IGP link-state routing.                                                                  |
-| *host*    | DefaultRoute and SSH services, representing an SSH server having a default route when connected directly to a router.                      |
 | *PC*      | DefaultRoute service for having a default route when connected directly to a router.                                                       |
 | *mdr*     | zebra, OSPFv3MDR, and IPForward services for wireless-optimized MANET Designated Router routing.                                           |
 | *prouter* | a physical router, having the same default services as the *router* node type; for incorporating Linux testbed machines into an emulation. |
@@ -48,84 +51,21 @@ Configuration files can be automatically generated by each service. For
 example, CORE automatically generates routing protocol configuration for the
 router nodes in order to simplify the creation of virtual networks.
 
-To change the services associated with a node, double-click on the node to
-invoke its configuration dialog and click on the *Services...* button,
-or right-click a node a choose *Services...* from the menu.
-Services are enabled or disabled by clicking on their names. The button next to
-each service name allows you to customize all aspects of this service for this
-node. For example, special route redistribution commands could be inserted in
-to the Quagga routing configuration associated with the zebra service.
-
-To change the default services associated with a node type, use the Node Types
-dialog available from the *Edit* button at the end of the Layer-3 nodes
-toolbar, or choose *Node types...* from the  *Session* menu. Note that
-any new services selected are not applied to existing nodes if the nodes have
-been customized.
-
-## Customizing a Service
-
-A service can be fully customized for a particular node. From the node's
-configuration dialog, click on the button next to the service name to invoke
-the service customization dialog for that service.
-The dialog has three tabs for configuring the different aspects of the service:
-files, directories, and startup/shutdown.
-
-!!! note
-
-    A **yellow** customize icon next to a service indicates that service
-    requires customization (e.g. the *Firewall* service).
-    A **green** customize icon indicates that a custom configuration exists.
-    Click the *Defaults* button when customizing a service to remove any
-    customizations.
-
-The Files tab is used to display or edit the configuration files or scripts that
-are used for this service. Files can be selected from a drop-down list, and
-their contents are displayed in a text entry below. The file contents are
-generated by the CORE daemon based on the network topology that exists at
-the time the customization dialog is invoked.
-
-The Directories tab shows the per-node directories for this service. For the
-default types, CORE nodes share the same filesystem tree, except for these
-per-node directories that are defined by the services. For example, the
-**/var/run/quagga** directory needs to be unique for each node running
-the Zebra service, because Quagga running on each node needs to write separate
-PID files to that directory.
-
-!!! note
+To change the services associated with a node, right-click a node a choose
+**Services...** from the menu button. Services are enabled or disabled by selecting
+through the service groups and enabling the checkboxes on services. Select a selected
+service and click the **Configure** button to further configure a given service.
 
-    The **/var/log** and **/var/run** directories are
-    mounted uniquely per-node by default.
-    Per-node mount targets can be found in **/tmp/pycore.<session id>/<node name>.conf/**
-
-The Startup/shutdown tab lists commands that are used to start and stop this
-service. The startup index allows configuring when this service starts relative
-to the other services enabled for this node; a service with a lower startup
-index value is started before those with higher values. Because shell scripts
-generated by the Files tab will not have execute permissions set, the startup
-commands should include the shell name, with
-something like ```sh script.sh```.
-
-Shutdown commands optionally terminate the process(es) associated with this
-service. Generally they send a kill signal to the running process using the
-*kill* or *killall* commands. If the service does not terminate
-the running processes using a shutdown command, the processes will be killed
-when the *vnoded* daemon is terminated (with *kill -9*) and
-the namespace destroyed. It is a good practice to
-specify shutdown commands, which will allow for proper process termination, and
-for run-time control of stopping and restarting services.
-
-Validate commands are executed following the startup commands. A validate
-command can execute a process or script that should return zero if the service
-has started successfully, and have a non-zero return value for services that
-have had a problem starting. For example, the *pidof* command will check
-if a process is running and return zero when found. When a validate command
-produces a non-zero return value, an exception is generated, which will cause
-an error to be displayed in the Check Emulation Light.
+To change the default services associated with a node type, use the **Custom Nodes**
+option under the *Edit* menu option. Here you can define new node types, with a custom
+icon, and a custom set of services to start on nodes of this type. This node type
+will be added to the container node options on the left toolbar, allowing for easy
+drag and drop creation for nodes of this type.
 
-!!! note
-
-    To start, stop, and restart services during run-time, right-click a
-    node and use the *Services...* menu.
+The node types are saved in the GUI config file **~/.coregui/config.yaml**.
+Keep this in mind when changing the default services for
+existing node types; it may be better to simply create a new node type. It is
+recommended that you do not change the default built-in node types.
 
 ## New Services
 
@@ -133,167 +73,120 @@ Services can save time required to configure nodes, especially if a number
 of nodes require similar configuration procedures. New services can be
 introduced to automate tasks.
 
-### Leveraging UserDefined
-
-The easiest way to capture the configuration of a new process into a service
-is by using the **UserDefined** service. This is a blank service where any
-aspect may be customized. The UserDefined service is convenient for testing
-ideas for a service before adding a new service type.
-
 ### Creating New Services
 
 !!! note
 
-    The directory name used in **custom_services_dir** below should be unique and
-    should not correspond to any existing Python module name. For example, don't
-    use the name **subprocess** or **services**.
+    The directory base name used in **custom_services_dir** below should
+    be unique and should not correspond to any existing Python module name.
+    For example, don't use the name **subprocess** or **services**.
 
 1. Modify the example service shown below
    to do what you want. It could generate config/script files, mount per-node
-   directories, start processes/scripts, etc. sample.py is a Python file that
-   defines one or more classes to be imported. You can create multiple Python
-   files that will be imported.
+   directories, start processes/scripts, etc. Your file can define one or more
+   classes to be imported. You can create multiple Python files that will be imported.
 
-2. Put these files in a directory such as `/home/<user>/.coregui/custom_services`
-   Note that the last component of this directory name **custom_services** should not
-   be named the same as any python module, due to naming conflicts.
+2. Put these files in a directory such as **~/.coregui/custom_services**.
 
-3. Add a **custom_services_dir = `/home/<user>/.coregui/custom_services`** entry to the
+3. Set the **custom_services_dir = ~/.coregui/custom_services** entry to the
    **/opt/core/etc/core.conf** file.
 
 4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax)
-   should be displayed in the daemon output.
+   should be displayed in the terminal (or service log, like journalctl).
 
 5. Start using your custom service on your nodes. You can create a new node
    type that uses your service, or change the default services for an existing
    node type, or change individual nodes.
 
-If you have created a new service type that may be useful to others, please
-consider contributing it to the CORE project.
-
-#### Example Custom Service
+### Example Custom Service
 
 Below is the skeleton for a custom service with some documentation. Most
 people would likely only setup the required class variables **(name/group)**.
-Then define the **configs** (files they want to generate) and implement the
-**generate_config** function to dynamically create the files wanted. Finally
-the **startup** commands would be supplied, which typically tends to be
+Then define the **files** to generate and implement the
+**get_text_template** function to dynamically create the files wanted. Finally,
+the **startup** commands would be supplied, which typically tend to be
 running the shell files generated.
 
 ```python
-"""
-Simple example custom service, used to drive shell commands on a node.
-"""
-from typing import Tuple
-
-from core.nodes.base import CoreNode
-from core.services.coreservices import CoreService, ServiceMode
-
-
-class ExampleService(CoreService):
-    """
-    Example Custom CORE Service
-
-    :cvar name: name used as a unique ID for this service and is required, no spaces
-    :cvar group: allows you to group services within the GUI under a common name
-    :cvar executables: executables this service depends on to function, if executable is
-        not on the path, service will not be loaded
-    :cvar dependencies: services that this service depends on for startup, tuple of
-        service names
-    :cvar dirs: directories that this service will create within a node
-    :cvar configs: files that this service will generate, without a full path this file
-        goes in the node's directory e.g. /tmp/pycore.12345/n1.conf/myfile
-    :cvar startup: commands used to start this service, any non-zero exit code will
-        cause a failure
-    :cvar validate: commands used to validate that a service was started, any non-zero
-        exit code will cause a failure
-    :cvar validation_mode: validation mode, used to determine startup success.
-        NON_BLOCKING    - runs startup commands, and validates success with validation commands
-        BLOCKING        - runs startup commands, and validates success with the startup commands themselves
-        TIMER           - runs startup commands, and validates success by waiting for "validation_timer" alone
-    :cvar validation_timer: time in seconds for a service to wait for validation, before
-        determining success in TIMER/NON_BLOCKING modes.
-    :cvar validation_period: period in seconds to wait before retrying validation,
-        only used in NON_BLOCKING mode
-    :cvar shutdown: shutdown commands to stop this service
-    """
-
-    name: str = "ExampleService"
-    group: str = "Utility"
-    executables: Tuple[str, ...] = ()
-    dependencies: Tuple[str, ...] = ()
-    dirs: Tuple[str, ...] = ()
-    configs: Tuple[str, ...] = ("myservice1.sh", "myservice2.sh")
-    startup: Tuple[str, ...] = tuple(f"sh {x}" for x in configs)
-    validate: Tuple[str, ...] = ()
-    validation_mode: ServiceMode = ServiceMode.NON_BLOCKING
-    validation_timer: int = 5
-    validation_period: float = 0.5
-    shutdown: Tuple[str, ...] = ()
-
-    @classmethod
-    def on_load(cls) -> None:
-        """
-        Provides a way to run some arbitrary logic when the service is loaded, possibly
-        to help facilitate dynamic settings for the environment.
-
-        :return: nothing
+from typing import Dict, List
+
+from core.config import ConfigString, ConfigBool, Configuration
+from core.services.base import Service, ServiceMode, ShadowDir
+
+
+# class that subclasses Service
+class ExampleService(Service):
+    # unique name for your service within CORE
+    name: str = "Example"
+    # the group your service is associated with, used for display in GUI
+    group: str = "ExampleGroup"
+    # directories that the service should shadow mount, hiding the system directory
+    directories: List[str] = [
+        "/usr/local/core",
+    ]
+    # files that this service should generate, defaults to nodes home directory
+    # or can provide an absolute path to a mounted directory
+    files: List[str] = [
+        "example-start.sh",
+        "/usr/local/core/file1",
+    ]
+    # executables that should exist on path, that this service depends on
+    executables: List[str] = []
+    # other services that this service depends on, can be used to define service start order
+    dependencies: List[str] = []
+    # commands to run to start this service
+    startup: List[str] = []
+    # commands to run to validate this service
+    validate: List[str] = []
+    # commands to run to stop this service
+    shutdown: List[str] = []
+    # validation mode, blocking, non-blocking, and timer
+    validation_mode: ServiceMode = ServiceMode.BLOCKING
+    # configurable values that this service can use, for file generation
+    default_configs: List[Configuration] = [
+        ConfigString(id="value1", label="Text"),
+        ConfigBool(id="value2", label="Boolean"),
+        ConfigString(id="value3", label="Multiple Choice", options=["value1", "value2", "value3"]),
+    ]
+    # sets of values to set for the configuration defined above, can be used to
+    # provide convenient sets of values to typically use
+    modes: Dict[str, Dict[str, str]] = {
+        "mode1": {"value1": "value1", "value2": "0", "value3": "value2"},
+        "mode2": {"value1": "value2", "value2": "1", "value3": "value3"},
+        "mode3": {"value1": "value3", "value2": "0", "value3": "value1"},
+    }
+    # defines directories that this service can help shadow within a node
+    shadow_directories: List[ShadowDir] = [
+        ShadowDir(path="/user/local/core", src="/opt/core")
+    ]
+
+    def get_text_template(self, name: str) -> str:
+        return """
+        # sample script 1
+        # node id(${node.id}) name(${node.name})
+        # config: ${config}
+        echo hello
         """
-        pass
-
-    @classmethod
-    def get_configs(cls, node: CoreNode) -> Tuple[str, ...]:
-        """
-        Provides a way to dynamically generate the config files from the node a service
-        will run. Defaults to the class definition and can be left out entirely if not
-        needed.
+```
 
-        :param node: core node that the service is being ran on
-        :return: tuple of config files to create
-        """
-        return cls.configs
+#### Validation Mode
 
-    @classmethod
-    def generate_config(cls, node: CoreNode, filename: str) -> str:
-        """
-        Returns a string representation for a file, given the node the service is
-        starting on the config filename that this information will be used for. This
-        must be defined, if "configs" are defined.
+Validation modes are used to determine if a service has started up successfully.
 
-        :param node: core node that the service is being ran on
-        :param filename: configuration file to generate
-        :return: configuration file content
-        """
-        cfg = "#!/bin/sh\n"
-        if filename == cls.configs[0]:
-            cfg += "# auto-generated by MyService (sample.py)\n"
-            for iface in node.get_ifaces():
-                cfg += f'echo "Node {node.name} has interface {iface.name}"\n'
-        elif filename == cls.configs[1]:
-            cfg += "echo hello"
-        return cfg
-
-    @classmethod
-    def get_startup(cls, node: CoreNode) -> Tuple[str, ...]:
-        """
-        Provides a way to dynamically generate the startup commands from the node a
-        service will run. Defaults to the class definition and can be left out entirely
-        if not needed.
+* blocking - startup commands are expected to run til completion and return 0 exit code
+* non-blocking - startup commands are ran, but do not wait for completion
+* timer - startup commands are ran, and an arbitrary amount of time is waited to consider started
 
-        :param node: core node that the service is being ran on
-        :return: tuple of startup commands to run
-        """
-        return cls.startup
+#### Shadow Directories
 
-    @classmethod
-    def get_validate(cls, node: CoreNode) -> Tuple[str, ...]:
-        """
-        Provides a way to dynamically generate the validate commands from the node a
-        service will run. Defaults to the class definition and can be left out entirely
-        if not needed.
+Shadow directories provide a convenience for copying a directory and the files within
+it to a nodes home directory, to allow a unique set of per node files.
 
-        :param node: core node that the service is being ran on
-        :return: tuple of commands to validate service startup with
-        """
-        return cls.validate
-```
+* `ShadowDir(path="/user/local/core")` - copies files at the given location into the node
+* `ShadowDir(path="/user/local/core", src="/opt/core")` - copies files to the given location,
+  but sourced from the provided location
+* `ShadowDir(path="/user/local/core", templates=True)` - copies files and treats them as
+  templates for generation
+* `ShadowDir(path="/user/local/core", has_node_paths=True)` - copies files from the given
+  location, and looks for unique node names directories within it, using a directory named
+  default, when not preset
diff --git a/docs/tutorials/setup.md b/docs/tutorials/setup.md
index 581626900..ae5924530 100644
--- a/docs/tutorials/setup.md
+++ b/docs/tutorials/setup.md
@@ -70,10 +70,10 @@ optional arguments:
 
 ### Installing the Chat App Service
 
-1. You will first need to edit **/opt/core/etc/core.conf** to update the config
+1. You will first need to edit **/opt/core/etc/core.conf** to update the custom
    service path to pick up your service
     ``` shell
-    custom_config_services_dir = <path for service>
+    custom_services_dir = <path for service>
     ```
 2. Then you will need to copy/move **chatapp/chatapp_service.py** to the directory
    configured above
diff --git a/mkdocs.yml b/mkdocs.yml
index 03504b131..7109609b1 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -58,8 +58,7 @@ nav:
           - Docker: docker.md
           - LXC: lxc.md
       - Services:
-          - Config Services: configservices.md
-          - Services (Deprecated): services.md
+          - Overview: services.md
           - Provided:
               - Bird: services/bird.md
               - EMANE: services/emane.md
diff --git a/package/etc/core.conf b/package/etc/core.conf
index 1923250d4..6feea60f0 100644
--- a/package/etc/core.conf
+++ b/package/etc/core.conf
@@ -11,7 +11,6 @@ frr_sbin_search = "/usr/local/sbin /usr/sbin /usr/lib/frr /usr/libexec/frr"
 # this may be a comma-separated list, and directory names should be unique
 # and not named 'services'
 #custom_services_dir = /home/<user>/.coregui/custom_services
-#custom_config_services_dir = /home/<user>/.coregui/custom_services
 
 # uncomment to  establish a standalone control backchannel for accessing nodes
 # (overriden by the session option of the same name)
diff --git a/package/share/examples/configservices/switch.py b/package/share/examples/configservices/switch.py
index 937c3aa83..af0f48a2e 100644
--- a/package/share/examples/configservices/switch.py
+++ b/package/share/examples/configservices/switch.py
@@ -18,7 +18,7 @@
 
     # node one
     options = CoreNode.create_options()
-    options.config_services = ["DefaultRoute", "IPForward"]
+    options.services = ["DefaultRoute", "IPForward"]
     node1 = session.add_node(CoreNode, options=options)
     interface = prefixes.create_iface(node1)
     session.add_link(node1.id, switch.id, iface1_data=interface)

From 245cc3420eb1b3db5f50e2b41719256745f1e924 Mon Sep 17 00:00:00 2001
From: Blake Harnden <32446120+bharnden@users.noreply.github.com>
Date: Tue, 26 Sep 2023 16:29:07 -0700
Subject: [PATCH 7/7] daemon/doc: adjustments to how services and their
 configurations are parsed and written to xml, update to documentation to
 correct invalid code

---
 daemon/core/xml/corexml.py                    | 73 ++++++-------------
 docs/python.md                                | 47 +++++-------
 .../share/tutorials/tutorial1/scenario.xml    | 10 +--
 .../tutorials/tutorial1/scenario_service.xml  | 13 ++--
 .../share/tutorials/tutorial2/scenario.xml    | 25 ++-----
 .../share/tutorials/tutorial3/scenario.xml    | 25 ++-----
 .../share/tutorials/tutorial5/scenario.xml    |  9 +--
 .../tutorial6/completed-scenario.xml          | 25 ++-----
 .../share/tutorials/tutorial7/scenario.xml    | 18 ++---
 .../tutorials/tutorial7/scenario_service.xml  | 18 ++---
 10 files changed, 86 insertions(+), 177 deletions(-)

diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py
index 35aa65c1e..b07e2729d 100644
--- a/daemon/core/xml/corexml.py
+++ b/daemon/core/xml/corexml.py
@@ -4,19 +4,24 @@
 
 from lxml import etree
 
-import core.nodes.base
-import core.nodes.physical
 from core import utils
 from core.config import Configuration
 from core.emane.nodes import EmaneNet, EmaneOptions
 from core.emulator.data import InterfaceData, LinkOptions
 from core.emulator.enumerations import EventTypes, NodeTypes
 from core.errors import CoreXmlError
-from core.nodes.base import CoreNodeBase, CoreNodeOptions, NodeBase, Position
+from core.nodes.base import (
+    CoreNetworkBase,
+    CoreNodeBase,
+    CoreNodeOptions,
+    NodeBase,
+    Position,
+)
 from core.nodes.docker import DockerNode, DockerOptions
 from core.nodes.interface import CoreInterface
 from core.nodes.lxd import LxcNode, LxcOptions
 from core.nodes.network import CtrlNet, GreTapBridge, PtpNet, WlanNode
+from core.nodes.physical import Rj45Node
 from core.nodes.podman import PodmanNode, PodmanOptions
 from core.nodes.wireless import WirelessNode
 
@@ -148,8 +153,9 @@ def add_position(self) -> None:
 
 
 class DeviceElement(NodeElement):
-    def __init__(self, session: "Session", node: NodeBase) -> None:
+    def __init__(self, session: "Session", node: CoreNodeBase) -> None:
         super().__init__(session, node, "device")
+        self.node: CoreNodeBase = node
         add_attribute(self.element, "type", node.model)
         self.add_class()
         self.add_services()
@@ -171,10 +177,6 @@ def add_class(self) -> None:
 
     def add_services(self) -> None:
         service_elements = etree.Element("services")
-        if service_elements.getchildren():
-            self.element.append(service_elements)
-
-        service_elements = etree.Element("configservices")
         for name, service in self.node.services.items():
             etree.SubElement(service_elements, "service", name=name)
         if service_elements.getchildren():
@@ -251,7 +253,6 @@ def write_session_origin(self) -> None:
         add_attribute(origin, "lon", lon)
         add_attribute(origin, "alt", alt)
         has_origin = len(origin.items()) > 0
-
         if has_origin:
             self.scenario.append(origin)
             refscale = self.session.location.refscale
@@ -281,7 +282,6 @@ def write_session_hooks(self) -> None:
                 add_attribute(hook, "name", file_name)
                 add_attribute(hook, "state", state.value)
                 hook.text = data
-
         if hooks.getchildren():
             self.scenario.append(hooks)
 
@@ -299,11 +299,9 @@ def write_session_metadata(self) -> None:
         config = self.session.metadata
         if not config:
             return
-
         for key in config:
             value = config[key]
             add_configuration(metadata_elements, key, value)
-
         if metadata_elements.getchildren():
             self.scenario.append(metadata_elements)
 
@@ -329,7 +327,6 @@ def write_mobility_configs(self) -> None:
             all_configs = self.session.mobility.get_all_configs(node_id)
             if not all_configs:
                 continue
-
             for model_name in all_configs:
                 config = all_configs[model_name]
                 logger.debug(
@@ -343,19 +340,16 @@ def write_mobility_configs(self) -> None:
                 for name in config:
                     value = config[name]
                     add_configuration(mobility_configuration, name, value)
-
         if mobility_configurations.getchildren():
             self.scenario.append(mobility_configurations)
 
     def write_service_configs(self) -> None:
-        service_configurations = etree.Element("configservice_configurations")
+        service_configurations = etree.Element("service_configurations")
         for node in self.session.nodes.values():
             if not isinstance(node, CoreNodeBase):
                 continue
             for name, service in node.services.items():
-                service_element = etree.SubElement(
-                    service_configurations, "service", name=name
-                )
+                service_element = etree.Element("service", name=name)
                 add_attribute(service_element, "node", node.id)
                 if service.custom_config:
                     configs_element = etree.SubElement(service_element, "configs")
@@ -370,6 +364,8 @@ def write_service_configs(self) -> None:
                             templates_element, "template", name=template_name
                         )
                         template_element.text = etree.CDATA(template)
+                if service.custom_config or service.custom_templates:
+                    service_configurations.append(service_element)
         if service_configurations.getchildren():
             self.scenario.append(service_configurations)
 
@@ -385,15 +381,13 @@ def write_default_services(self) -> None:
     def write_nodes(self) -> None:
         for node in self.session.nodes.values():
             # network node
-            is_network_or_rj45 = isinstance(
-                node, (core.nodes.base.CoreNetworkBase, core.nodes.physical.Rj45Node)
-            )
+            is_network_or_rj45 = isinstance(node, (CoreNetworkBase, Rj45Node))
             is_controlnet = isinstance(node, CtrlNet)
             is_ptp = isinstance(node, PtpNet)
             if is_network_or_rj45 and not (is_controlnet or is_ptp):
                 self.write_network(node)
             # device node
-            elif isinstance(node, core.nodes.base.CoreNodeBase):
+            elif isinstance(node, CoreNodeBase):
                 self.write_device(node)
 
     def write_network(self, node: NodeBase) -> None:
@@ -418,7 +412,7 @@ def write_links(self) -> None:
         if link_elements.getchildren():
             self.scenario.append(link_elements)
 
-    def write_device(self, node: NodeBase) -> None:
+    def write_device(self, node: CoreNodeBase) -> None:
         device = DeviceElement(self.session, node)
         self.devices.append(device.element)
 
@@ -426,7 +420,7 @@ def create_iface_element(
         self, element_name: str, iface: CoreInterface
     ) -> etree.Element:
         iface_element = etree.Element(element_name)
-        # check if interface if connected to emane
+        # check if interface is connected to emane
         if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet):
             nem_id = self.session.emane.get_nem_id(iface)
             add_attribute(iface_element, "nem", nem_id)
@@ -499,7 +493,6 @@ def __init__(self, session: "Session") -> None:
     def read(self, file_path: Path) -> None:
         xml_tree = etree.parse(str(file_path))
         self.scenario = xml_tree.getroot()
-
         # read xml session content
         self.read_default_services()
         self.read_session_metadata()
@@ -517,7 +510,6 @@ def read_default_services(self) -> None:
         default_services = self.scenario.find("default_services")
         if default_services is None:
             return
-
         for node in default_services.iterchildren():
             model = node.get("type")
             services = []
@@ -529,7 +521,6 @@ def read_session_metadata(self) -> None:
         session_metadata = self.scenario.find("session_metadata")
         if session_metadata is None:
             return
-
         configs = {}
         for data in session_metadata.iterchildren():
             name = data.get("name")
@@ -554,7 +545,6 @@ def read_session_hooks(self) -> None:
         session_hooks = self.scenario.find("session_hooks")
         if session_hooks is None:
             return
-
         for hook in session_hooks.iterchildren():
             name = hook.get("name")
             state = get_int(hook, "state")
@@ -577,19 +567,16 @@ def read_session_origin(self) -> None:
         session_origin = self.scenario.find("session_origin")
         if session_origin is None:
             return
-
         lat = get_float(session_origin, "lat")
         lon = get_float(session_origin, "lon")
         alt = get_float(session_origin, "alt")
         if all([lat, lon, alt]):
             logger.info("reading session reference geo: %s, %s, %s", lat, lon, alt)
             self.session.location.setrefgeo(lat, lon, alt)
-
         scale = get_float(session_origin, "scale")
         if scale:
             logger.info("reading session reference scale: %s", scale)
             self.session.location.refscale = scale
-
         x = get_float(session_origin, "x")
         y = get_float(session_origin, "y")
         z = get_float(session_origin, "z")
@@ -606,7 +593,6 @@ def read_emane_configs(self) -> None:
             iface_id = get_int(emane_configuration, "iface")
             model_name = emane_configuration.get("model")
             configs = {}
-
             # validate node and model
             node = self.session.nodes.get(node_id)
             if not node:
@@ -616,7 +602,6 @@ def read_emane_configs(self) -> None:
                 raise CoreXmlError(
                     f"invalid interface id({iface_id}) for node({node.name})"
                 )
-
             # read and set emane model configuration
             platform_configuration = emane_configuration.find("platform")
             for config in platform_configuration.iterchildren():
@@ -638,7 +623,6 @@ def read_emane_configs(self) -> None:
                 name = config.get("name")
                 value = config.get("value")
                 configs[name] = value
-
             logger.info(
                 "reading emane configuration node(%s) model(%s)", node_id, model_name
             )
@@ -649,17 +633,14 @@ def read_mobility_configs(self) -> None:
         mobility_configurations = self.scenario.find("mobility_configurations")
         if mobility_configurations is None:
             return
-
         for mobility_configuration in mobility_configurations.iterchildren():
             node_id = get_int(mobility_configuration, "node")
             model_name = mobility_configuration.get("model")
             configs = {}
-
             for config in mobility_configuration.iterchildren():
                 name = config.get("name")
                 value = config.get("value")
                 configs[name] = value
-
             logger.info(
                 "reading mobility configuration node(%s) model(%s)", node_id, model_name
             )
@@ -670,7 +651,6 @@ def read_nodes(self) -> None:
         if device_elements is not None:
             for device_element in device_elements.iterchildren():
                 self.read_device(device_element)
-
         network_elements = self.scenario.find("networks")
         if network_elements is not None:
             for network_element in network_elements.iterchildren():
@@ -699,7 +679,9 @@ def read_device(self, device_element: etree.Element) -> None:
         # check for special options
         if isinstance(options, CoreNodeOptions):
             options.model = model
-            service_elements = device_element.find("configservices")
+            service_elements = device_element.find("services")
+            if service_elements is None:
+                service_elements = device_element.find("configservices")
             if service_elements is not None:
                 options.services.extend(
                     x.get("name") for x in service_elements.iterchildren()
@@ -762,16 +744,16 @@ def read_network(self, network_element: etree.Element) -> None:
                 node.set_config(config)
 
     def read_service_configs(self) -> None:
-        service_configs = self.scenario.find("configservice_configurations")
+        service_configs = self.scenario.find("service_configurations")
+        if service_configs is None:
+            service_configs = self.scenario.find("configservice_configurations")
         if service_configs is None:
             return
-
         for service_element in service_configs.iterchildren():
             name = service_element.get("name")
             node_id = get_int(service_element, "node")
             node = self.session.get_node(node_id, CoreNodeBase)
             service = node.services[name]
-
             configs_element = service_element.find("configs")
             if configs_element is not None:
                 config = {}
@@ -780,7 +762,6 @@ def read_service_configs(self) -> None:
                     value = config_element.get("value")
                     config[key] = value
                 service.set_config(config)
-
             templates_element = service_element.find("templates")
             if templates_element is not None:
                 for template_element in templates_element.iterchildren():
@@ -795,7 +776,6 @@ def read_links(self) -> None:
         link_elements = self.scenario.find("links")
         if link_elements is None:
             return
-
         node_sets = set()
         for link_element in link_elements.iterchildren():
             node1_id = get_int(link_element, "node1")
@@ -805,21 +785,18 @@ def read_links(self) -> None:
             if node2_id is None:
                 node2_id = get_int(link_element, "node_two")
             node_set = frozenset((node1_id, node2_id))
-
             iface1_element = link_element.find("iface1")
             if iface1_element is None:
                 iface1_element = link_element.find("interface_one")
             iface1_data = None
             if iface1_element is not None:
                 iface1_data = create_iface_data(iface1_element)
-
             iface2_element = link_element.find("iface2")
             if iface2_element is None:
                 iface2_element = link_element.find("interface_two")
             iface2_data = None
             if iface2_element is not None:
                 iface2_data = create_iface_data(iface2_element)
-
             options_element = link_element.find("options")
             options = LinkOptions()
             if options_element is not None:
@@ -836,7 +813,6 @@ def read_links(self) -> None:
                     options.loss = get_float(options_element, "per")
                 options.unidirectional = get_int(options_element, "unidirectional") == 1
                 options.buffer = get_int(options_element, "buffer")
-
             if options.unidirectional and node_set in node_sets:
                 logger.info("updating link node1(%s) node2(%s)", node1_id, node2_id)
                 self.session.update_link(
@@ -847,5 +823,4 @@ def read_links(self) -> None:
                 self.session.add_link(
                     node1_id, node2_id, iface1_data, iface2_data, options
                 )
-
             node_sets.add(node_set)
diff --git a/docs/python.md b/docs/python.md
index 43e289c79..7fc0c4a69 100644
--- a/docs/python.md
+++ b/docs/python.md
@@ -320,15 +320,9 @@ n1 = session.add_node(CoreNode, position=position, options=options)
 position = Position(x=300, y=100)
 n2 = session.add_node(CoreNode, position=position, options=options)
 
-# configure general emane settings
-config = session.emane.get_configs()
-config.update({
-    "eventservicettl": "2"
-})
-
-# configure emane model settings
-# using a dict mapping currently support values as strings
-session.emane.set_model_config(emane.id, EmaneIeee80211abgModel.name, {
+# configure emane model using a dict, which currently support values as strings
+session.emane.set_config(emane.id, EmaneIeee80211abgModel.name, {
+    "eventservicettl": "2",
     "unicastrate": "3",
 })
 
@@ -366,44 +360,39 @@ session.emane.set_config(config_id, EmaneIeee80211abgModel.name, {
 
 Services help generate and run bash scripts on nodes for a given purpose.
 
-Configuring the files of a service results in a specific hard coded script being
+Configuring the templates of a service results in a specific hard coded script being
 generated, instead of the default scripts, that may leverage dynamic generation.
 
 The following features can be configured for a service:
 
-* configs - files that will be generated
-* dirs - directories that will be mounted unique to the node
+* files - files that will be generated
+* directories - directories that will be mounted unique to the node
 * startup - commands to run start a service
 * validate - commands to run to validate a service
 * shutdown - commands to run to stop a service
 
 Editing service properties:
-
 ```python
 # configure a service, for a node, for a given session
-session.services.set_service(node_id, service_name)
-service = session.services.get_service(node_id, service_name)
-service.configs = ("file1.sh", "file2.sh")
-service.dirs = ("/etc/node",)
-service.startup = ("bash file1.sh",)
-service.validate = ()
-service.shutdown = ()
+node = session.get_node(node_id, CoreNode)
+service = node.services[service_name]
+service.files = ["file1.sh", "file2.sh"]
+service.directories = ["/etc/node"]
+service.startup = ["bash file1.sh"]
+service.validate = []
+service.shutdown = []
 ```
 
-When editing a service file, it must be the name of `config`
-file that the service will generate.
+When editing a service file, it must be the name of
+`file` that the service will generate.
 
 Editing a service file:
-
 ```python
 # to edit the contents of a generated file you can specify
 # the service, the file name, and its contents
-session.services.set_service_file(
-    node_id,
-    service_name,
-    file_name,
-    "echo hello",
-)
+node = session.get_node(node_id, CoreNode)
+service = node.services[service_name]
+service.set_template(file_name, "echo hello")
 ```
 
 ## File Examples
diff --git a/package/share/tutorials/tutorial1/scenario.xml b/package/share/tutorials/tutorial1/scenario.xml
index 428fe4ca6..d429948ed 100644
--- a/package/share/tutorials/tutorial1/scenario.xml
+++ b/package/share/tutorials/tutorial1/scenario.xml
@@ -1,18 +1,18 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmpa_gpqzdf">
+<scenario>
   <networks/>
   <devices>
     <device id="1" name="n1" icon="" canvas="1" type="PC" class="" image="">
       <position x="250.0" y="250.0" lat="47.57679395743362" lon="-122.12891511224676" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="DefaultRoute"/>
-      </configservices>
+      </services>
     </device>
     <device id="2" name="n2" icon="" canvas="1" type="PC" class="" image="">
       <position x="500.0" y="250.0" lat="47.576775777287395" lon="-122.12534430899237" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="DefaultRoute"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
diff --git a/package/share/tutorials/tutorial1/scenario_service.xml b/package/share/tutorials/tutorial1/scenario_service.xml
index ab092f4c2..3dd66e492 100644
--- a/package/share/tutorials/tutorial1/scenario_service.xml
+++ b/package/share/tutorials/tutorial1/scenario_service.xml
@@ -1,19 +1,19 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmp2exmy1y9">
+<scenario>
   <networks/>
   <devices>
     <device id="1" name="n1" icon="" canvas="0" type="PC" class="" image="">
       <position x="250.0" y="250.0" lat="47.576893948125004" lon="-122.12895553643455" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="ChatApp Server"/>
         <service name="DefaultRoute"/>
-      </configservices>
+      </services>
     </device>
     <device id="2" name="n2" icon="" canvas="0" type="PC" class="" image="">
       <position x="500.0" y="250.0" lat="47.576893948125004" lon="-122.12558685411909" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="DefaultRoute"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -34,9 +34,6 @@
       <configuration name="link_timeout" value="4"/>
     </core>
   </emane_global_configuration>
-  <configservice_configurations>
-    <service name="ChatApp Server" node="1"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial2/scenario.xml b/package/share/tutorials/tutorial2/scenario.xml
index ee60f7922..cb0386f1d 100644
--- a/package/share/tutorials/tutorial2/scenario.xml
+++ b/package/share/tutorials/tutorial2/scenario.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmp3n8ocfk5">
+<scenario>
   <networks>
     <network id="1" name="wlan1" icon="" canvas="0" model="basic_range" mobility="ns2script" type="WIRELESS_LAN">
       <position x="200.0" y="200.0" lat="47.57735226369077" lon="-122.1296216435031" alt="2.0"/>
@@ -8,27 +8,27 @@
   <devices>
     <device id="2" name="n2" icon="" canvas="0" type="mdr" class="" image="">
       <position x="100.0" y="100.0" lat="47.57826125326112" lon="-122.13096911642927" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
     <device id="3" name="n3" icon="" canvas="0" type="mdr" class="" image="">
       <position x="300.0" y="100.0" lat="47.57826125326112" lon="-122.12827417057692" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
     <device id="4" name="n4" icon="" canvas="0" type="mdr" class="" image="">
       <position x="500.0" y="100.0" lat="47.57826125326112" lon="-122.12557922472458" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -42,17 +42,6 @@
       <iface1 id="0" name="eth0" mac="00:16:3e:77:c9:d3" ip4="10.0.0.4" ip4_mask="24" ip6="2001::4" ip6_mask="64"/>
     </link>
   </links>
-  <configservice_configurations>
-    <service name="zebra" node="2"/>
-    <service name="OSPFv3MDR" node="2"/>
-    <service name="IPForward" node="2"/>
-    <service name="zebra" node="3"/>
-    <service name="OSPFv3MDR" node="3"/>
-    <service name="IPForward" node="3"/>
-    <service name="zebra" node="4"/>
-    <service name="OSPFv3MDR" node="4"/>
-    <service name="IPForward" node="4"/>
-  </configservice_configurations>
   <session_origin lat="47.57917022705078" lon="-122.13231658935547" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial3/scenario.xml b/package/share/tutorials/tutorial3/scenario.xml
index dbe68d4d1..50c25dc6d 100644
--- a/package/share/tutorials/tutorial3/scenario.xml
+++ b/package/share/tutorials/tutorial3/scenario.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmpnpymmhg9">
+<scenario>
   <networks>
     <network id="3" name="wlan3" icon="" canvas="1" model="basic_range" mobility="ns2script" type="WIRELESS_LAN">
       <position x="294.0" y="149.0" lat="47.57781203554751" lon="-122.12836264834701" alt="2.0"/>
@@ -8,27 +8,27 @@
   <devices>
     <device id="1" name="n1" icon="" canvas="1" type="mdr" class="" image="">
       <position x="208.0" y="211.0" lat="47.57724845903762" lon="-122.12952147506353" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
         <service name="zebra"/>
-      </configservices>
+      </services>
     </device>
     <device id="2" name="n2" icon="" canvas="1" type="mdr" class="" image="">
       <position x="393.0" y="223.0" lat="47.57713937901246" lon="-122.1270286501501" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
         <service name="zebra"/>
-      </configservices>
+      </services>
     </device>
     <device id="4" name="n4" icon="" canvas="1" type="mdr" class="" image="">
       <position x="499.0" y="186.0" lat="47.577475708360176" lon="-122.12560032884835" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
         <service name="zebra"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -62,17 +62,6 @@
       <configuration name="script_stop" value=""/>
     </mobility_configuration>
   </mobility_configurations>
-  <configservice_configurations>
-    <service name="IPForward" node="1"/>
-    <service name="OSPFv3MDR" node="1"/>
-    <service name="zebra" node="1"/>
-    <service name="IPForward" node="2"/>
-    <service name="OSPFv3MDR" node="2"/>
-    <service name="zebra" node="2"/>
-    <service name="IPForward" node="4"/>
-    <service name="OSPFv3MDR" node="4"/>
-    <service name="zebra" node="4"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial5/scenario.xml b/package/share/tutorials/tutorial5/scenario.xml
index 05d93045c..619f0a074 100644
--- a/package/share/tutorials/tutorial5/scenario.xml
+++ b/package/share/tutorials/tutorial5/scenario.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmp8mbqm1qx">
+<scenario>
   <networks>
     <network id="2" name="unassigned" icon="" canvas="1" type="RJ45">
       <position x="235.0" y="255.0" lat="47.5768484978344" lon="-122.12915765737347" alt="2.0"/>
@@ -8,9 +8,9 @@
   <devices>
     <device id="1" name="n1" icon="" canvas="1" type="PC" class="" image="">
       <position x="395.0" y="189.0" lat="47.57744843849355" lon="-122.12700170069158" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="DefaultRoute"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -25,9 +25,6 @@
       <options delay="0" bandwidth="0" loss="0.0" dup="0" jitter="0" unidirectional="1" buffer="0"/>
     </link>
   </links>
-  <configservice_configurations>
-    <service name="DefaultRoute" node="1"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial6/completed-scenario.xml b/package/share/tutorials/tutorial6/completed-scenario.xml
index 2b9857278..e4f5fcaf6 100644
--- a/package/share/tutorials/tutorial6/completed-scenario.xml
+++ b/package/share/tutorials/tutorial6/completed-scenario.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmpaghphwl7">
+<scenario>
   <networks>
     <network id="4" name="n4" icon="" canvas="1" type="WIRELESS">
       <position x="170.0" y="184.0" lat="47.577493888263376" lon="-122.13003351477549" alt="2.0"/>
@@ -18,27 +18,27 @@
   <devices>
     <device id="1" name="n1" icon="/usr/share/core/examples/tutorials/tutorial6/drone.png" canvas="1" type="mdr" class="" image="">
       <position x="303.0" y="25.0" lat="47.57893917036898" lon="-122.12824137578366" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
-      </configservices>
+      </services>
     </device>
     <device id="2" name="n2" icon="/usr/share/core/examples/tutorials/tutorial6/drone.png" canvas="1" type="mdr" class="" image="">
       <position x="205.0" y="158.0" lat="47.57773022643051" lon="-122.12956189925131" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
-      </configservices>
+      </services>
     </device>
     <device id="3" name="n3" icon="/usr/share/core/examples/tutorials/tutorial6/drone.png" canvas="1" type="mdr" class="" image="">
       <position x="120.0" y="316.0" lat="47.57629400111251" lon="-122.13070725123856" alt="2.0"/>
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="IPForward"/>
         <service name="OSPFv3MDR"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -52,17 +52,6 @@
       <iface1 id="0" name="eth0" mac="00:00:00:aa:00:02" ip4="10.0.0.3" ip4_mask="32" ip6="2001::3" ip6_mask="128"/>
     </link>
   </links>
-  <configservice_configurations>
-    <service name="zebra" node="1"/>
-    <service name="IPForward" node="1"/>
-    <service name="OSPFv3MDR" node="1"/>
-    <service name="zebra" node="2"/>
-    <service name="IPForward" node="2"/>
-    <service name="OSPFv3MDR" node="2"/>
-    <service name="zebra" node="3"/>
-    <service name="IPForward" node="3"/>
-    <service name="OSPFv3MDR" node="3"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial7/scenario.xml b/package/share/tutorials/tutorial7/scenario.xml
index 721a7b8f9..1893383f3 100644
--- a/package/share/tutorials/tutorial7/scenario.xml
+++ b/package/share/tutorials/tutorial7/scenario.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmpmewk97ls">
+<scenario>
   <networks>
     <network id="1" name="emane1" icon="" canvas="1" model="emane_ieee80211abg" type="EMANE">
       <position x="375.0" y="500.0" />
@@ -8,19 +8,19 @@
   <devices>
     <device id="2" name="n2" icon="" canvas="1" type="mdr" class="" image="">
       <position x="250.0" y="250.0" />
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
     <device id="3" name="n3" icon="" canvas="1" type="mdr" class="" image="">
       <position x="500.0" y="250.0" />
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -31,14 +31,6 @@
       <iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
     </link>
   </links>
-  <configservice_configurations>
-    <service name="zebra" node="2"/>
-    <service name="OSPFv3MDR" node="2"/>
-    <service name="IPForward" node="2"/>
-    <service name="zebra" node="3"/>
-    <service name="OSPFv3MDR" node="3"/>
-    <service name="IPForward" node="3"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>
diff --git a/package/share/tutorials/tutorial7/scenario_service.xml b/package/share/tutorials/tutorial7/scenario_service.xml
index da2cb8e85..063c5712b 100644
--- a/package/share/tutorials/tutorial7/scenario_service.xml
+++ b/package/share/tutorials/tutorial7/scenario_service.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<scenario name="/tmp/tmpmewk97ls">
+<scenario>
   <networks>
     <network id="1" name="emane1" icon="" canvas="1" model="emane_ieee80211abg" type="EMANE">
       <position x="375.0" y="500.0" />
@@ -8,20 +8,20 @@
   <devices>
     <device id="2" name="n2" icon="" canvas="1" type="mdr" class="" image="">
       <position x="250.0" y="250.0" />
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
         <service name="ChatApp Server"/>
-      </configservices>
+      </services>
     </device>
     <device id="3" name="n3" icon="" canvas="1" type="mdr" class="" image="">
       <position x="500.0" y="250.0" />
-      <configservices>
+      <services>
         <service name="zebra"/>
         <service name="OSPFv3MDR"/>
         <service name="IPForward"/>
-      </configservices>
+      </services>
     </device>
   </devices>
   <links>
@@ -32,14 +32,6 @@
       <iface1 nem="2" id="0" name="eth0" mac="02:02:00:00:00:02" ip4="10.0.0.2" ip4_mask="32" ip6="2001::2" ip6_mask="128"/>
     </link>
   </links>
-  <configservice_configurations>
-    <service name="zebra" node="2"/>
-    <service name="OSPFv3MDR" node="2"/>
-    <service name="IPForward" node="2"/>
-    <service name="zebra" node="3"/>
-    <service name="OSPFv3MDR" node="3"/>
-    <service name="IPForward" node="3"/>
-  </configservice_configurations>
   <session_origin lat="47.579166412353516" lon="-122.13232421875" alt="2.0" scale="150.0"/>
   <session_options>
     <configuration name="controlnet" value=""/>