diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 4d6f02461..5768a0837 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -13,11 +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, -) from core.api.grpc.core_pb2 import ( ExecuteScriptRequest, GetConfigRequest, @@ -39,12 +34,10 @@ SetMobilityConfigRequest, ) from core.api.grpc.services_pb2 import ( - GetNodeServiceFileRequest, GetNodeServiceRequest, GetServiceDefaultsRequest, + GetServiceRenderedRequest, ServiceActionRequest, - ServiceDefaults, - SetServiceDefaultsRequest, ) from core.api.grpc.wlan_pb2 import ( GetWlanConfigRequest, @@ -725,79 +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, @@ -822,30 +742,6 @@ def service_action( response = self.stub.ServiceAction(request) return response.result - def config_service_action( - self, - session_id: int, - node_id: int, - service: str, - action: wrappers.ServiceAction, - ) -> bool: - """ - Send an action to a config service for a node. - - :param session_id: session id - :param node_id: node id - :param service: config 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.ConfigServiceAction(request) - return response.result - def get_wlan_config( self, session_id: int, node_id: int ) -> dict[str, wrappers.ConfigOption]: @@ -970,28 +866,28 @@ 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.ConfigServiceDefaults: + ) -> 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 = GetConfigServiceDefaultsRequest( + request = GetServiceDefaultsRequest( name=name, session_id=session_id, node_id=node_id ) - response = self.stub.GetConfigServiceDefaults(request) - return wrappers.ConfigServiceDefaults.from_proto(response) + 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 @@ -999,27 +895,27 @@ 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( + 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 :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 8d6b593d4..e6610ccd6 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -9,15 +9,8 @@ from grpc import ServicerContext from core import utils -from core.api.grpc import common_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.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 +33,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 @@ -81,7 +73,6 @@ def add_node_data( 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 if isinstance(options, (DockerOptions, LxcOptions, PodmanOptions)): @@ -303,12 +294,11 @@ 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 = [] + 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) @@ -346,26 +336,12 @@ 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): - 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] = ConfigServiceConfig( - node_id=node.id, - name=service.name, + service_configs[service.name] = services_pb2.ServiceConfig( templates=service.custom_templates, config=service.custom_config, ) @@ -377,10 +353,9 @@ def get_node_proto( type=node_type.value, position=position, geo=geo, - services=services, icon=node.icon, image=image, - config_services=config_services, + services=services, dir=node_dir, channel=channel, canvas=node.canvas, @@ -388,7 +363,6 @@ def get_node_proto( wireless_config=wireless_config, mobility_config=mobility_config, service_configs=service_configs, - config_service_configs=config_service_configs, emane_configs=emane_configs, ) @@ -626,49 +600,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 +701,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 +742,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 +754,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,30 +800,14 @@ 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 node.service_configs: 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.config_service_configs.items(): - service = core_node.config_services[service_name] + for service_name, service_config in node.service_configs.items(): + 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 0f48d38b4..610409581 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -15,21 +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, -) +from core.api.grpc import common_pb2, core_pb2, core_pb2_grpc, grpcutils, services_pb2 from core.api.grpc.core_pb2 import ( ExecuteScriptResponse, GetWirelessConfigRequest, @@ -67,18 +53,15 @@ SetMobilityConfigResponse, ) from core.api.grpc.services_pb2 import ( - GetNodeServiceFileRequest, - GetNodeServiceFileResponse, GetNodeServiceRequest, GetNodeServiceResponse, GetServiceDefaultsRequest, GetServiceDefaultsResponse, - Service, + GetServiceRenderedRequest, + GetServiceRenderedResponse, ServiceAction, ServiceActionRequest, ServiceActionResponse, - SetServiceDefaultsRequest, - SetServiceDefaultsResponse, ) from core.api.grpc.wlan_pb2 import ( GetWlanConfigRequest, @@ -88,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 @@ -104,7 +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.coreservices import ServiceManager +from core.services.base import Service, ServiceBootError from core.xml.corexml import CoreXmlWriter logger = logging.getLogger(__name__) @@ -212,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. @@ -232,13 +212,8 @@ 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( + service_proto = services_pb2.Service( name=service.name, group=service.group, executables=service.executables, @@ -252,11 +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=services, - config_services=config_services, emane_models=emane_models, ) @@ -922,124 +896,11 @@ 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: - """ - 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 @@ -1049,15 +910,17 @@ def ConfigServiceAction( 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: service.start() result = True - except ConfigServiceBootError: + except ServiceBootError: pass elif request.action == ServiceAction.STOP: service.stop() @@ -1067,13 +930,13 @@ def ConfigServiceAction( 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) @@ -1235,57 +1098,57 @@ 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. + 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: 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. + 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}" ) 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. + 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) @@ -1305,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 cee87cd1b..179a2ba24 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -5,19 +5,7 @@ from google.protobuf.internal.containers import MessageMap -from core.api.grpc import ( - common_pb2, - configservices_pb2, - core_pb2, - emane_pb2, - services_pb2, -) - - -class ConfigServiceValidationMode(Enum): - BLOCKING = 0 - NON_BLOCKING = 1 - TIMER = 2 +from core.api.grpc import common_pb2, core_pb2, emane_pb2, services_pb2 class ServiceValidationMode(Enum): @@ -112,7 +100,7 @@ class EventType: @dataclass -class ConfigService: +class Service: group: str name: str executables: list[str] @@ -122,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: configservices_pb2.ConfigService) -> "ConfigService": - return ConfigService( + def from_proto(cls, proto: services_pb2.Service) -> "Service": + return Service( group=proto.group, name=proto.name, executables=list(proto.executables), @@ -138,50 +126,46 @@ def from_proto(cls, proto: configservices_pb2.ConfigService) -> "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: configservices_pb2.ConfigServiceConfig - ) -> "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]] @classmethod def from_proto( - cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse - ) -> "ConfigServiceDefaults": + cls, proto: services_pb2.GetServiceDefaultsResponse + ) -> "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 ) @@ -199,16 +183,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 +193,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 @@ -724,7 +603,6 @@ class Node: 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 image: str = None @@ -741,32 +619,19 @@ 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 - ) + service_configs: dict[str, ServiceData] = 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 model = emane_config.model 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(): - config_service_configs[service] = ConfigServiceData( + service_configs = {} + for service, service_config in proto.service_configs.items(): + service_configs[service] = ServiceData( templates=dict(service_config.templates), config=dict(service_config.config), ) @@ -777,7 +642,6 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node": 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, image=proto.image, @@ -789,8 +653,6 @@ def from_proto(cls, proto: core_pb2.Node) -> "Node": 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), ) @@ -807,21 +669,8 @@ def to_proto(self) -> core_pb2.Node: ) 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( + 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( @@ -831,7 +680,6 @@ def to_proto(self) -> core_pb2.Node: model=self.model, position=self.position.to_proto(), services=self.services, - config_services=self.config_services, emane=self.emane, icon=self.icon, image=self.image, @@ -842,7 +690,6 @@ def to_proto(self) -> core_pb2.Node: 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()}, ) @@ -994,16 +841,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/configservices/utilservices/__init__.py b/daemon/core/configservices/utilservices/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 574002e6f..3b31089c4 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -3,11 +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.coreservices import ServiceManager +from core.services.manager import ServiceManager logger = logging.getLogger(__name__) @@ -37,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 @@ -65,19 +64,9 @@ 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 - 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 a6804ea71..b8772ada1 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,7 +57,7 @@ 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.services.manager import ServiceManager from core.xml import corexml, corexmldeployment from core.xml.corexml import CoreXmlReader, CoreXmlWriter @@ -152,12 +151,11 @@ 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) - # config services - self.service_manager: Optional[ConfigServiceManager] = None + # services + self.service_manager: Optional[ServiceManager] = None @classmethod def get_node_class(cls, _type: NodeTypes) -> type[NodeBase]: @@ -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,9 +1052,7 @@ 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, (), {})) + funcs.append((node.stop_services, (), {})) utils.threadpool(funcs) # shutdown emane @@ -1089,13 +1084,11 @@ def boot_node(self, node: CoreNode) -> None: :return: nothing """ logger.info( - "booting node(%s): config services(%s) services(%s)", + "booting node(%s): services(%s)", node.name, - ", ".join(node.config_services.keys()), - ", ".join(x.name for x in node.services), + ", ".join(node.services.keys()), ) - self.services.boot_services(node) - 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 d34eaa42d..44778dfb2 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -13,11 +13,9 @@ 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, - ConfigServiceDefaults, EmaneModelConfig, Event, ExceptionEvent, @@ -27,12 +25,11 @@ MessageType, Node, NodeEvent, - NodeServiceData, NodeType, Position, Server, - ServiceConfig, - ServiceFileConfig, + Service, + ServiceDefaults, Session, SessionLocation, SessionState, @@ -76,9 +73,8 @@ 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] = {} + self.services_groups: dict[str, set[str]] = {} + self.services: dict[str, Service] = {} # loaded configuration data self.emane_models: list[str] = [] @@ -356,17 +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: - 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( - 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 @@ -558,30 +549,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 @@ -636,12 +603,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, @@ -716,65 +683,11 @@ 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) + 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 - ) -> 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 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/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py deleted file mode 100644 index 0e873a796..000000000 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ /dev/null @@ -1,414 +0,0 @@ -""" -Service configuration dialog -""" -import logging -import tkinter as tk -from tkinter import ttk -from typing import TYPE_CHECKING, Optional - -import grpc - -from core.api.grpc.wrappers import ( - ConfigOption, - ConfigServiceData, - Node, - ServiceValidationMode, -) -from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADX, PADY -from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from core.gui.app import Application - from core.gui.coreclient import CoreClient - - -class ConfigServiceConfigDialog(Dialog): - def __init__( - self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node - ) -> None: - title = f"{service_name} Config Service" - super().__init__(app, title, master=master) - self.core: "CoreClient" = app.core - self.node: Node = node - self.service_name: str = service_name - self.radiovar: tk.IntVar = tk.IntVar(value=2) - self.directories: list[str] = [] - self.templates: list[str] = [] - self.rendered: dict[str, 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: tk.DoubleVar = tk.DoubleVar() - self.modes: list[str] = [] - self.mode_configs: dict[str, dict[str, str]] = {} - self.notebook: Optional[ttk.Notebook] = None - self.templates_combobox: Optional[ttk.Combobox] = None - self.modes_combobox: Optional[ttk.Combobox] = 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.template_text: Optional[CodeText] = None - self.rendered_text: Optional[CodeText] = None - self.validation_period_entry: Optional[ttk.Entry] = None - self.original_service_files: dict[str, str] = {} - self.temp_service_files: dict[str, str] = {} - self.modified_files: set[str] = set() - self.config_frame: Optional[ConfigFrame] = None - self.default_config: dict[str, str] = {} - self.config: dict[str, ConfigOption] = {} - 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) - service = self.core.config_services[self.service_name] - self.dependencies = service.dependencies[:] - self.executables = service.executables[:] - self.directories = service.directories[:] - self.templates = service.files[:] - self.startup_commands = service.startup[:] - self.validation_commands = service.validate[:] - self.shutdown_commands = service.shutdown[:] - self.validation_mode = service.validation_mode - self.validation_time = service.validation_timer - self.validation_period.set(service.validation_period) - defaults = self.core.get_config_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.node.id, self.service_name - ) - service_config = self.node.config_service_configs.get(self.service_name) - if service_config: - for key, value in service_config.config.items(): - self.config[key].value = value - logger.info("default config: %s", self.default_config) - for file, data in service_config.templates.items(): - self.modified_files.add(file) - self.temp_service_files[file] = data - except grpc.RpcError as e: - self.app.show_grpc_exception("Get Config Service Error", e) - self.has_error = True - - def draw(self) -> None: - self.top.columnconfigure(0, weight=1) - self.top.rowconfigure(0, weight=1) - # draw notebook - self.notebook = ttk.Notebook(self.top) - self.notebook.grid(sticky=tk.NSEW, pady=PADY) - self.draw_tab_files() - if self.config: - self.draw_tab_config() - self.draw_tab_startstop() - self.draw_tab_validation() - self.draw_buttons() - - def draw_tab_files(self) -> 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/Files") - - label = ttk.Label( - tab, text="Directories and templates that will be used for this service." - ) - label.grid(pady=PADY) - - frame = ttk.Frame(tab) - frame.grid(sticky=tk.EW, pady=PADY) - frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Directories") - label.grid(row=0, column=0, sticky=tk.W, padx=PADX) - state = "readonly" if self.directories else tk.DISABLED - directories_combobox = ttk.Combobox(frame, values=self.directories, state=state) - directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) - if self.directories: - directories_combobox.current(0) - label = ttk.Label(frame, text="Files") - label.grid(row=1, column=0, sticky=tk.W, padx=PADX) - state = "readonly" if self.templates else tk.DISABLED - self.templates_combobox = ttk.Combobox( - frame, values=self.templates, state=state - ) - self.templates_combobox.bind( - "<>", self.handle_template_changed - ) - self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY) - # draw file template tab - notebook = ttk.Notebook(tab) - notebook.rowconfigure(0, weight=1) - notebook.columnconfigure(0, weight=1) - notebook.grid(sticky=tk.NSEW, pady=PADY) - # draw rendered file tab - rendered_tab = ttk.Frame(notebook, padding=FRAME_PAD) - rendered_tab.grid(sticky=tk.NSEW) - rendered_tab.rowconfigure(0, weight=1) - rendered_tab.columnconfigure(0, weight=1) - notebook.add(rendered_tab, text="Rendered") - self.rendered_text = CodeText(rendered_tab) - self.rendered_text.grid(sticky=tk.NSEW) - self.rendered_text.text.bind("", self.update_template_file_data) - # draw template file tab - template_tab = ttk.Frame(notebook, padding=FRAME_PAD) - template_tab.grid(sticky=tk.NSEW) - template_tab.rowconfigure(0, weight=1) - template_tab.columnconfigure(0, weight=1) - notebook.add(template_tab, text="Template") - self.template_text = CodeText(template_tab) - self.template_text.grid(sticky=tk.NSEW) - self.template_text.text.bind("", self.update_template_file_data) - if self.templates: - self.templates_combobox.current(0) - template_name = self.templates[0] - temp_data = self.temp_service_files[template_name] - self.template_text.set_text(temp_data) - rendered_data = self.rendered[template_name] - self.rendered_text.set_text(rendered_data) - else: - self.template_text.text.configure(state=tk.DISABLED) - self.rendered_text.text.configure(state=tk.DISABLED) - - def draw_tab_config(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") - - if self.modes: - frame = ttk.Frame(tab) - frame.grid(sticky=tk.EW, pady=PADY) - frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Modes") - label.grid(row=0, column=0, padx=PADX) - self.modes_combobox = ttk.Combobox( - frame, values=self.modes, state="readonly" - ) - self.modes_combobox.bind("<>", 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) - self.config_frame = ConfigFrame(tab, self.app, self.config) - self.config_frame.draw_config() - self.config_frame.grid(sticky=tk.NSEW, pady=PADY) - tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) - - def draw_tab_startstop(self) -> None: - tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky=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(0, weight=1) - label_frame.grid(row=i, column=0, sticky=tk.NSEW, pady=PADY) - listbox_scroll = ListboxScroll(label_frame) - for command in commands: - listbox_scroll.listbox.insert("end", command) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(sticky=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_validation(self) -> None: - tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky=tk.EW) - tab.columnconfigure(0, weight=1) - self.notebook.add(tab, text="Validation", 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", str(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=self.validation_period - ) - 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 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) - current_listbox.itemconfig(current_listbox.curselection()[0], bg="") - self.destroy() - return - service_config = self.node.config_service_configs.setdefault( - self.service_name, ConfigServiceData() - ) - if self.config_frame: - self.config_frame.parse_config() - service_config.config = {x.name: x.value for x in self.config.values()} - for file in self.modified_files: - service_config.templates[file] = self.temp_service_files[file] - all_current = current_listbox.get(0, tk.END) - current_listbox.itemconfig(all_current.index(self.service_name), bg="green") - self.destroy() - - def handle_template_changed(self, event: tk.Event) -> None: - template_name = self.templates_combobox.get() - temp_data = self.temp_service_files[template_name] - self.template_text.set_text(temp_data) - rendered = self.rendered[template_name] - self.rendered_text.set_text(rendered) - - def handle_mode_changed(self, event: tk.Event) -> None: - mode = self.modes_combobox.get() - config = self.mode_configs[mode] - logger.info("mode config: %s", config) - self.config_frame.set_values(config) - - def update_template_file_data(self, _event: tk.Event) -> None: - template = self.templates_combobox.get() - self.temp_service_files[template] = self.rendered_text.get_text() - if self.rendered[template] != self.temp_service_files[template]: - self.modified_files.add(template) - return - self.temp_service_files[template] = self.template_text.get_text() - if self.temp_service_files[template] != self.original_service_files[template]: - self.modified_files.add(template) - else: - self.modified_files.discard(template) - - def is_custom(self) -> bool: - has_custom_templates = len(self.modified_files) > 0 - has_custom_config = False - if self.config_frame: - current = self.config_frame.parse_config() - has_custom_config = self.default_config != current - return has_custom_templates or has_custom_config - - def click_defaults(self) -> None: - # clear all saved state data - self.modified_files.clear() - self.node.config_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 - ) - # reset current selected file data and config data, if present - template_name = self.templates_combobox.get() - temp_data = self.temp_service_files[template_name] - self.template_text.set_text(temp_data) - rendered_data = self.rendered[template_name] - self.rendered_text.set_text(rendered_data) - if self.config_frame: - 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: - for cmd in to_add: - commands.append(cmd) - listbox.insert(tk.END, cmd) 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/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("<>", 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/nodeconfigservice.py deleted file mode 100644 index ce718080f..000000000 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -core node services -""" -import logging -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.configserviceconfig import ConfigServiceConfigDialog -from core.gui.dialogs.dialog import Dialog -from core.gui.themes import FRAME_PAD, PADX, PADY -from core.gui.widgets import CheckboxList, ListboxScroll - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from core.gui.app import Application - - -class NodeConfigServiceDialog(Dialog): - def __init__( - self, app: "Application", node: Node, services: set[str] = None - ) -> None: - title = f"{node.name} Config Services" - super().__init__(app, title) - self.node: Node = node - self.groups: Optional[ListboxScroll] = None - self.services: Optional[CheckboxList] = None - self.current: Optional[ListboxScroll] = None - if services is None: - services = set(node.config_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.config_services_groups): - self.groups.listbox.insert(tk.END, group) - self.groups.listbox.bind("<>", 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) - self.draw_current_services() - - 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.config_services_groups[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.config_service_configs.pop(name, None) - self.draw_current_services() - self.node.config_services = self.current_services.copy() - - def click_configure(self) -> None: - current_selection = self.current.listbox.curselection() - if len(current_selection): - dialog = ConfigServiceConfigDialog( - self, - self.app, - self.current.listbox.get(current_selection[0]), - self.node, - ) - if not dialog.has_error: - dialog.show() - self.draw_current_services() - else: - messagebox.showinfo( - "Config Service Configuration", - "Select a service to configure", - parent=self, - ) - - def draw_current_services(self) -> 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") - - 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.destroy() - - def click_cancel(self) -> None: - self.current_services = None - 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.config_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 diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 66e83fa44..04fed934e 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -1,6 +1,7 @@ """ core node services """ +import logging import tkinter as tk from tkinter import messagebox, ttk from typing import TYPE_CHECKING, Optional @@ -11,19 +12,24 @@ from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll +logger = logging.getLogger(__name__) + 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)" + def __init__( + self, app: "Application", node: Node, services: set[str] = None + ) -> None: + title = f"{node.name} Services" 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) + if services is None: + services = set(node.services) self.current_services: set[str] = services self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.draw() @@ -43,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.services): + for group in sorted(self.app.core.services_groups): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) @@ -61,12 +67,10 @@ def draw(self) -> None: 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") + self.draw_current_services() frame = ttk.Frame(self.top) frame.grid(stick="ew") @@ -90,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.services[group]): + for name in sorted(self.app.core.services_groups[group]): checked = name in self.current_services self.services.add(name, checked) @@ -100,12 +104,7 @@ def service_clicked(self, name: str, var: tk.IntVar) -> None: 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.draw_current_services() self.node.services = self.current_services.copy() def click_configure(self) -> None: @@ -117,22 +116,30 @@ def click_configure(self) -> None: 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() + self.draw_current_services() else: messagebox.showinfo( - "Service Configuration", "Select a service to configure", parent=self + "Service Configuration", + "Select a service to configure", + parent=self, ) - def click_cancel(self) -> None: - self.destroy() + def draw_current_services(self) -> 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") def click_save(self) -> None: self.node.services = self.current_services.copy() + logger.info("saved node services: %s", self.node.services) + self.destroy() + + def click_cancel(self) -> None: + self.current_services = None self.destroy() def click_remove(self) -> None: @@ -142,13 +149,10 @@ def click_remove(self) -> None: 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 + return service in self.node.service_configs diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 5eec7fafa..a48536ec9 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -1,19 +1,22 @@ +""" +Service configuration dialog +""" import logging import tkinter as tk -from pathlib import Path -from tkinter import filedialog, messagebox, ttk +from tkinter import 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.api.grpc.wrappers import ( + ConfigOption, + Node, + ServiceData, + ServiceValidationMode, +) 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 +from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll logger = logging.getLogger(__name__) @@ -21,21 +24,20 @@ 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)" + title = f"{service_name} Service" 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.directories: list[str] = [] + self.templates: list[str] = [] + self.rendered: dict[str, str] = {} self.dependencies: list[str] = [] self.executables: list[str] = [] self.startup_commands: list[str] = [] @@ -46,31 +48,26 @@ def __init__( 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.validation_period: tk.DoubleVar = tk.DoubleVar() + self.modes: list[str] = [] + self.mode_configs: dict[str, dict[str, str]] = {} 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.templates_combobox: Optional[ttk.Combobox] = None + self.modes_combobox: Optional[ttk.Combobox] = 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.template_text: Optional[CodeText] = None + self.rendered_text: 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.config_frame: Optional[ConfigFrame] = None + self.default_config: dict[str, str] = {} + self.config: dict[str, ConfigOption] = {} self.has_error: bool = False self.load() if not self.has_error: @@ -79,180 +76,141 @@ def __init__( def load(self) -> None: try: self.core.start_session(definition=True) - default_config = self.app.core.get_node_service( + service = self.core.services[self.service_name] + self.dependencies = service.dependencies[:] + self.executables = service.executables[:] + self.directories = service.directories[:] + self.templates = service.files[:] + self.startup_commands = service.startup[:] + self.validation_commands = service.validate[:] + self.shutdown_commands = service.shutdown[:] + self.validation_mode = service.validation_mode + self.validation_time = service.validation_timer + self.validation_period.set(service.validation_period) + 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_service_rendered( 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 + 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 + logger.info("default config: %s", self.default_config) + for file, data in service_config.templates.items(): + self.modified_files.add(file) + self.temp_service_files[file] = data except grpc.RpcError as e: - self.app.show_grpc_exception("Get Node Service Error", e) + self.app.show_grpc_exception("Get 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) - + self.top.rowconfigure(0, weight=1) # draw notebook self.notebook = ttk.Notebook(self.top) self.notebook.grid(sticky=tk.NSEW, pady=PADY) self.draw_tab_files() - self.draw_tab_directories() + if self.config: + self.draw_tab_config() self.draw_tab_startstop() - self.draw_tab_configuration() - + self.draw_tab_validation() 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") + tab.rowconfigure(2, weight=1) + self.notebook.add(tab, text="Directories/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( - "<>", 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 + tab, text="Directories and templates that will be used for this service." ) - 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) + label.grid(pady=PADY) 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, + label = ttk.Label(frame, text="Directories") + label.grid(row=0, column=0, sticky=tk.W, padx=PADX) + state = "readonly" if self.directories else tk.DISABLED + directories_combobox = ttk.Combobox(frame, values=self.directories, state=state) + directories_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) + if self.directories: + directories_combobox.current(0) + label = ttk.Label(frame, text="Files") + label.grid(row=1, column=0, sticky=tk.W, padx=PADX) + state = "readonly" if self.templates else tk.DISABLED + self.templates_combobox = ttk.Combobox( + frame, values=self.templates, state=state ) - 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( - "", self.update_temp_service_file_data + self.templates_combobox.bind( + "<>", self.handle_template_changed ) + self.templates_combobox.grid(row=1, column=1, sticky=tk.EW, pady=PADY) + # draw file template tab + notebook = ttk.Notebook(tab) + notebook.rowconfigure(0, weight=1) + notebook.columnconfigure(0, weight=1) + notebook.grid(sticky=tk.NSEW, pady=PADY) + # draw rendered file tab + rendered_tab = ttk.Frame(notebook, padding=FRAME_PAD) + rendered_tab.grid(sticky=tk.NSEW) + rendered_tab.rowconfigure(0, weight=1) + rendered_tab.columnconfigure(0, weight=1) + notebook.add(rendered_tab, text="Rendered") + self.rendered_text = CodeText(rendered_tab) + self.rendered_text.grid(sticky=tk.NSEW) + self.rendered_text.text.bind("", self.update_template_file_data) + # draw template file tab + template_tab = ttk.Frame(notebook, padding=FRAME_PAD) + template_tab.grid(sticky=tk.NSEW) + template_tab.rowconfigure(0, weight=1) + template_tab.columnconfigure(0, weight=1) + notebook.add(template_tab, text="Template") + self.template_text = CodeText(template_tab) + self.template_text.grid(sticky=tk.NSEW) + self.template_text.text.bind("", self.update_template_file_data) + if self.templates: + self.templates_combobox.current(0) + template_name = self.templates[0] + temp_data = self.temp_service_files[template_name] + self.template_text.set_text(temp_data) + rendered_data = self.rendered[template_name] + self.rendered_text.set_text(rendered_data) + else: + self.template_text.text.configure(state=tk.DISABLED) + self.rendered_text.text.configure(state=tk.DISABLED) - def draw_tab_directories(self) -> None: + def draw_tab_config(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") + self.notebook.add(tab, text="Configuration") + + if self.modes: + frame = ttk.Frame(tab) + frame.grid(sticky=tk.EW, pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Modes") + label.grid(row=0, column=0, padx=PADX) + self.modes_combobox = ttk.Combobox( + frame, values=self.modes, state="readonly" + ) + self.modes_combobox.bind("<>", self.handle_mode_changed) + self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) - 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("<>", 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) + 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) + tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1) def draw_tab_startstop(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) @@ -281,26 +239,13 @@ def draw_tab_startstop(self) -> None: ) commands = self.validation_commands label_frame.columnconfigure(0, weight=1) - label_frame.rowconfigure(1, weight=1) + label_frame.rowconfigure(0, 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("", 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("", self.delete_command) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.bind("<>", 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) + listbox_scroll.grid(sticky=tk.NSEW) if i == 0: self.startup_commands_listbox = listbox_scroll.listbox elif i == 1: @@ -308,11 +253,11 @@ def draw_tab_startstop(self) -> None: elif i == 2: self.validate_commands_listbox = listbox_scroll.listbox - def draw_tab_configuration(self) -> None: + def draw_tab_validation(self) -> None: tab = ttk.Frame(self.notebook, padding=FRAME_PAD) - tab.grid(sticky=tk.NSEW) + tab.grid(sticky=tk.EW) tab.columnconfigure(0, weight=1) - self.notebook.add(tab, text="Configuration", sticky=tk.NSEW) + self.notebook.add(tab, text="Validation", sticky=tk.NSEW) frame = ttk.Frame(tab) frame.grid(sticky=tk.EW, pady=PADY) @@ -321,7 +266,7 @@ def draw_tab_configuration(self) -> None: 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.insert("end", str(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) @@ -343,7 +288,7 @@ def draw_tab_configuration(self) -> None: 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() + frame, state=tk.DISABLED, textvariable=self.validation_period ) self.validation_period_entry.grid(row=2, column=1, sticky=tk.EW, pady=PADY) @@ -370,243 +315,89 @@ def draw_tab_configuration(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) - - 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) + button.grid(row=0, column=2, sticky=tk.EW) 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() - ): + current_listbox = self.master.current.listbox + if not self.is_custom(): self.node.service_configs.pop(self.service_name, None) - self.current_service_color("") + current_listbox.itemconfig(current_listbox.curselection()[0], bg="") 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 + service_config = self.node.service_configs.setdefault( + self.service_name, ServiceData() + ) + if self.config_frame: + self.config_frame.parse_config() + service_config.config = {x.name: x.value for x in self.config.values()} 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") + service_config.templates[file] = self.temp_service_files[file] + all_current = current_listbox.get(0, tk.END) + current_listbox.itemconfig(all_current.index(self.service_name), bg="green") self.destroy() - 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) + def handle_template_changed(self, event: tk.Event) -> None: + template_name = self.templates_combobox.get() + temp_data = self.temp_service_files[template_name] + self.template_text.set_text(temp_data) + rendered = self.rendered[template_name] + self.rendered_text.set_text(rendered) + + def handle_mode_changed(self, event: tk.Event) -> None: + mode = self.modes_combobox.get() + config = self.mode_configs[mode] + logger.info("mode config: %s", config) + self.config_frame.set_values(config) + + def update_template_file_data(self, _event: tk.Event) -> None: + template = self.templates_combobox.get() + self.temp_service_files[template] = self.rendered_text.get_text() + if self.rendered[template] != self.temp_service_files[template]: + self.modified_files.add(template) + return + self.temp_service_files[template] = self.template_text.get_text() + if self.temp_service_files[template] != self.original_service_files[template]: + self.modified_files.add(template) else: - self.modified_files.discard(filename) + self.modified_files.discard(template) - 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 is_custom(self) -> bool: + has_custom_templates = len(self.modified_files) > 0 + has_custom_config = False + if self.config_frame: + current = self.config_frame.parse_config() + has_custom_config = self.default_config != current + return has_custom_templates or has_custom_config def click_defaults(self) -> None: - """ - clears out any custom configuration permanently - """ - # clear coreclient data + # clear all saved state data + self.modified_files.clear() 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("") + # reset session definition and retrieve default rendered templates + self.core.start_session(definition=True) + 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] + self.template_text.set_text(temp_data) + rendered_data = self.rendered[template_name] + self.rendered_text.set_text(rendered_data) + if self.config_frame: + logger.info("resetting defaults: %s", self.default_config) + self.config_frame.set_values(self.default_config) - 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] + self, 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/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 0cfbf2e97..c030ccda7 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -14,7 +14,6 @@ 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 @@ -243,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) @@ -260,12 +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 (Deprecated)", command=self.show_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 @@ -382,10 +376,6 @@ 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() - def has_emane_link(self, iface_id: int) -> Node: result = None for edge in self.edges: @@ -479,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 e59a89e40..0f048a2b3 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -9,28 +9,26 @@ 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 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.coreservices import CoreService + from core.services.base import Service - CoreServices = list[Union[CoreService, type[CoreService]]] - ConfigServiceType = type[ConfigService] + ServiceType = type[Service] PRIVATE_DIRS: list[Path] = [Path("/var/run"), Path("/var/log")] @@ -115,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): @@ -151,7 +145,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() @@ -388,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, "ConfigService"] = {} + self.services: dict[str, "Service"] = {} self.directory: Optional[Path] = None self.tmpnodedir: bool = False @@ -469,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: """ @@ -489,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 = ConfigServiceDependencies(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: @@ -589,18 +582,19 @@ 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) - for name in config_services: + # add services + services = options.services + if not services: + services = self.session.service_manager.defaults.get(self.model, []) + logger.info( + "setting node(%s) model(%s) services: %s", + self.name, + self.model, + 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/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/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/configservice/__init__.py b/daemon/core/services/defaults/__init__.py similarity index 100% rename from daemon/core/configservice/__init__.py rename to daemon/core/services/defaults/__init__.py diff --git a/daemon/core/configservices/__init__.py b/daemon/core/services/defaults/frrservices/__init__.py similarity index 100% rename from daemon/core/configservices/__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..e71eea62a 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]] = {} @@ -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): @@ -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/frrservices/__init__.py b/daemon/core/services/defaults/nrlservices/__init__.py similarity index 100% rename from daemon/core/configservices/frrservices/__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 80% rename from daemon/core/configservices/nrlservices/services.py rename to daemon/core/services/defaults/nrlservices/services.py index 3002cd94f..8815c89ba 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,19 +39,19 @@ 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]] = {} 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) return dict(has_smf=has_smf, ifnames=ifnames) -class NrlSmf(ConfigService): +class NrlSmf(Service): name: str = "SMF" group: str = GROUP directories: list[str] = [] @@ -61,13 +61,13 @@ 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]] = {} 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): @@ -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,13 +91,13 @@ 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]] = {} 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 @@ -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,19 +115,19 @@ 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]] = {} 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) 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,19 +137,19 @@ 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]] = {} 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) 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/nrlservices/__init__.py b/daemon/core/services/defaults/quaggaservices/__init__.py similarity index 100% rename from daemon/core/configservices/nrlservices/__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..3de0dbf46 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]] = {} @@ -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): @@ -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/quaggaservices/__init__.py b/daemon/core/services/defaults/securityservices/__init__.py similarity index 100% rename from daemon/core/configservices/quaggaservices/__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/securityservices/__init__.py b/daemon/core/services/defaults/utilservices/__init__.py similarity index 100% rename from daemon/core/configservices/securityservices/__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 83% rename from daemon/core/configservice/dependencies.py rename to daemon/core/services/dependencies.py index 1fbc4e48d..0b74a4266 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,18 +70,18 @@ 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 + :return: list of services to start in order """ logger.debug("starting service dependency check: %s", service.name) 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/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/configservice/manager.py b/daemon/core/services/manager.py similarity index 66% rename from daemon/core/configservice/manager.py rename to daemon/core/services/manager.py index 542f3cc51..c00aab5cd 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/services/manager.py @@ -3,25 +3,33 @@ 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]] = {} - - def get_service(self, name: str) -> type[ConfigService]: + self.services: dict[str, type[Service]] = {} + 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[Service]: """ Retrieve a service by name. @@ -34,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. @@ -56,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) @@ -92,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/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 "" "" -#{ - # 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 " -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 " -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 " -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 += """\ - - - StartServers 5 - MinSpareServers 5 - MaxSpareServers 10 - MaxClients 150 - MaxRequestsPerChild 0 - - - - StartServers 2 - MinSpareThreads 25 - MaxSpareThreads 75 - ThreadLimit 64 - ThreadsPerChild 25 - MaxClients 150 - MaxRequestsPerChild 0 - - - - StartServers 2 - MinSpareThreads 25 - MaxSpareThreads 75 - ThreadLimit 64 - ThreadsPerChild 25 - MaxClients 150 - MaxRequestsPerChild 0 - - -User ${APACHE_RUN_USER} -Group ${APACHE_RUN_GROUP} - -AccessFileName .htaccess - - -""" - cfg += permstr[version] - cfg += """\ - - -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 - - - Listen 443 - - - Listen 443 - - -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 - - - ServerAdmin webmaster@localhost - DocumentRoot /var/www - - Options FollowSymLinks - AllowOverride None - - - Options Indexes FollowSymLinks MultiViews - AllowOverride None -""" - cfg += permstr2[version] - cfg += """\ - - ErrorLog ${APACHE_LOG_DIR}/error.log - LogLevel warn - CustomLog ${APACHE_LOG_DIR}/access.log combined - - -""" - 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"""\ - -

{node.name} web server

-

This is the default web page for this server.

-

The web server software is running but no content has been added, yet.

-""" - for iface in node.get_ifaces(control=False): - body += f"
  • {iface.name} - {[str(x) for x in iface.ips()]}
  • \n" - return f"{body}" - - -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..b07e2729d 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -4,22 +4,26 @@ 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 -from core.services.coreservices import CoreService logger = logging.getLogger(__name__) @@ -148,71 +152,10 @@ 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: + 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() @@ -234,17 +177,11 @@ 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) + for name, service in self.node.services.items(): + etree.SubElement(service_elements, "service", name=name) if service_elements.getchildren(): self.element.append(service_elements) - config_service_elements = etree.Element("configservices") - for name, service in self.node.config_services.items(): - etree.SubElement(config_service_elements, "service", name=name) - if config_service_elements.getchildren(): - self.element.append(config_service_elements) - class NetworkElement(NodeElement): def __init__(self, session: "Session", node: NodeBase) -> None: @@ -291,7 +228,6 @@ def write_session(self) -> None: self.write_mobility_configs() self.write_emane_configs() self.write_service_configs() - self.write_configservice_configs() self.write_session_origin() self.write_servers() self.write_session_hooks() @@ -317,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 @@ -347,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) @@ -365,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) @@ -395,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( @@ -409,30 +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("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(): if not isinstance(node, CoreNodeBase): continue - for name, service in node.config_services.items(): - service_element = etree.SubElement( - service_configurations, "service", name=name - ) + for name, service in node.services.items(): + 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") @@ -447,13 +364,14 @@ def write_configservice_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) 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) @@ -463,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: @@ -496,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) @@ -504,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) @@ -577,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() @@ -585,31 +500,27 @@ 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() 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") if default_services is None: return - for node in default_services.iterchildren(): model = node.get("type") services = [] 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") if session_metadata is None: return - configs = {} for data in session_metadata.iterchildren(): name = data.get("name") @@ -634,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") @@ -657,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") @@ -677,50 +584,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: @@ -730,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: @@ -740,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(): @@ -762,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 ) @@ -773,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 ) @@ -794,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(): @@ -824,15 +680,12 @@ def read_device(self, device_element: etree.Element) -> None: if isinstance(options, CoreNodeOptions): options.model = model 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() ) - 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 @@ -890,18 +743,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("service_configurations") + if service_configs is 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] - - configs_element = configservice_element.find("configs") + service = node.services[name] + configs_element = service_element.find("configs") if configs_element is not None: config = {} for config_element in configs_element.iterchildren(): @@ -909,8 +762,7 @@ def read_configservice_configs(self) -> None: value = config_element.get("value") 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") @@ -924,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") @@ -934,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: @@ -965,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( @@ -976,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/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto deleted file mode 100644 index 25be616d7..000000000 --- a/daemon/proto/core/api/grpc/configservices.proto +++ /dev/null @@ -1,72 +0,0 @@ -syntax = "proto3"; - -package configservices; - -import "core/api/grpc/common.proto"; - -message ConfigServiceConfig { - int32 node_id = 1; - string name = 2; - map templates = 3; - map config = 4; -} - -message ConfigServiceValidationMode { - enum Enum { - BLOCKING = 0; - NON_BLOCKING = 1; - TIMER = 2; - } -} - -message ConfigService { - string group = 1; - string name = 2; - repeated string executables = 3; - repeated string dependencies = 4; - repeated string directories = 5; - repeated string files = 6; - repeated string startup = 7; - repeated string validate = 8; - repeated string shutdown = 9; - ConfigServiceValidationMode.Enum validation_mode = 10; - int32 validation_timer = 11; - float validation_period = 12; -} - -message ConfigMode { - string name = 1; - map config = 2; -} - -message GetConfigServiceDefaultsRequest { - string name = 1; - int32 session_id = 2; - int32 node_id = 3; -} - -message GetConfigServiceDefaultsResponse { - map templates = 1; - map config = 2; - repeated ConfigMode modes = 3; -} - -message GetNodeConfigServiceRequest { - int32 session_id = 1; - int32 node_id = 2; - string name = 3; -} - -message GetNodeConfigServiceResponse { - map config = 1; -} - -message GetConfigServiceRenderedRequest { - int32 session_id = 1; - int32 node_id = 2; - string name = 3; -} - -message GetConfigServiceRenderedResponse { - map rendered = 1; -} diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 14cb84338..9733f7dbe 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -2,11 +2,10 @@ 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"; -import "core/api/grpc/services.proto"; import "core/api/grpc/wlan.proto"; service CoreApi { @@ -72,26 +71,14 @@ service CoreApi { rpc MobilityAction (mobility.MobilityActionRequest) returns (mobility.MobilityActionResponse) { } - // service rpc + // services 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) { - } - rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) { - } - rpc ConfigServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) { - } - rpc GetConfigServiceRendered (configservices.GetConfigServiceRenderedRequest) returns (configservices.GetConfigServiceRenderedResponse) { + rpc GetServiceRendered (services.GetServiceRenderedRequest) returns (services.GetServiceRenderedResponse) { } // wlan rpc @@ -147,8 +134,7 @@ message GetConfigRequest { message GetConfigResponse { repeated services.Service services = 1; - repeated configservices.ConfigService config_services = 2; - repeated string emane_models = 3; + repeated string emane_models = 2; } @@ -585,22 +571,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 wlan_config = 16; - map mobility_config = 17; - map service_configs = 18; - map config_service_configs= 19; - repeated emane.NodeEmaneConfig emane_configs = 20; - map wireless_config = 21; + string emane = 6; + string icon = 7; + string image = 8; + string server = 9; + repeated string services = 10; + Geo geo = 11; + string dir = 12; + string channel = 13; + int32 canvas = 14; + map wlan_config = 15; + map mobility_config = 16; + map service_configs= 17; + repeated emane.NodeEmaneConfig emane_configs = 18; + map 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..bed288d5f 100644 --- a/daemon/proto/core/api/grpc/services.proto +++ b/daemon/proto/core/api/grpc/services.proto @@ -2,30 +2,7 @@ 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; - } -} +import "core/api/grpc/common.proto"; message ServiceAction { enum Enum { @@ -41,76 +18,78 @@ 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 ServiceActionRequest { + int32 session_id = 1; + int32 node_id = 2; + string service = 3; + ServiceAction.Enum action = 4; } -message NodeServiceConfig { - int32 node_id = 1; - string service = 2; - NodeServiceData data = 3; - map files = 4; +message ServiceActionResponse { + bool result = 1; } -message GetServiceDefaultsRequest { - int32 session_id = 1; +message ServiceConfig { + map templates = 1; + map config = 2; } -message GetServiceDefaultsResponse { - repeated ServiceDefaults defaults = 1; +message ServiceValidationMode { + enum Enum { + BLOCKING = 0; + NON_BLOCKING = 1; + TIMER = 2; + } } -message SetServiceDefaultsRequest { - int32 session_id = 1; - repeated ServiceDefaults defaults = 2; +message Service { + string group = 1; + string name = 2; + repeated string executables = 3; + repeated string dependencies = 4; + repeated string directories = 5; + repeated string files = 6; + repeated string startup = 7; + repeated string validate = 8; + repeated string shutdown = 9; + ServiceValidationMode.Enum validation_mode = 10; + int32 validation_timer = 11; + float validation_period = 12; } -message SetServiceDefaultsResponse { - bool result = 1; +message ConfigMode { + string name = 1; + map config = 2; } -message GetNodeServiceRequest { - int32 session_id = 1; - int32 node_id = 2; - string service = 3; +message GetServiceDefaultsRequest { + string name = 1; + int32 session_id = 2; + int32 node_id = 3; } -message GetNodeServiceResponse { - NodeServiceData service = 1; +message GetServiceDefaultsResponse { + map templates = 1; + map config = 2; + repeated ConfigMode modes = 3; } -message GetNodeServiceFileRequest { +message GetNodeServiceRequest { int32 session_id = 1; int32 node_id = 2; - string service = 3; - string file = 4; + string name = 3; } -message GetNodeServiceFileResponse { - string data = 1; +message GetNodeServiceResponse { + map config = 1; } -message ServiceActionRequest { +message GetServiceRenderedRequest { int32 session_id = 1; int32 node_id = 2; - string service = 3; - ServiceAction.Enum action = 4; + string name = 3; } -message ServiceActionResponse { - bool result = 1; +message GetServiceRenderedResponse { + map rendered = 1; } 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_config_services.py b/daemon/tests/test_config_services.py deleted file mode 100644 index 876b7f320..000000000 --- a/daemon/tests/test_config_services.py +++ /dev/null @@ -1,300 +0,0 @@ -from pathlib import Path -from unittest import mock - -import pytest - -from core.config import ConfigBool, ConfigString -from core.configservice.base import ( - ConfigService, - ConfigServiceBootError, - ConfigServiceMode, -) -from core.errors import CoreCommandError, CoreError - -TEMPLATE_TEXT = "echo hello" - - -class MyService(ConfigService): - name = "MyService" - group = "MyGroup" - directories = ["/usr/local/lib"] - files = ["test.sh"] - executables = [] - dependencies = [] - startup = [f"sh {files[0]}"] - validate = [f"pidof {files[0]}"] - shutdown = [f"pkill {files[0]}"] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [ - ConfigString(id="value1", label="Text"), - ConfigBool(id="value2", label="Boolean"), - ConfigString( - id="value3", label="Multiple Choice", options=["value1", "value2", "value3"] - ), - ] - modes = { - "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, - "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, - "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, - } - - def get_text_template(self, name: str) -> str: - return TEMPLATE_TEXT - - -class TestConfigServices: - def test_set_template(self): - # given - node = mock.MagicMock() - text = "echo custom" - service = MyService(node) - - # when - service.set_template(MyService.files[0], text) - - # then - assert MyService.files[0] in service.custom_templates - assert service.custom_templates[MyService.files[0]] == text - - def test_create_directories(self): - # given - node = mock.MagicMock() - service = MyService(node) - - # when - service.create_dirs() - - # then - directory = Path(MyService.directories[0]) - node.create_dir.assert_called_with(directory) - - def test_create_files_custom(self): - # given - node = mock.MagicMock() - service = MyService(node) - text = "echo custom" - service.set_template(MyService.files[0], text) - - # when - service.create_files() - - # then - file_path = Path(MyService.files[0]) - node.create_file.assert_called_with(file_path, text) - - def test_create_files_text(self): - # given - node = mock.MagicMock() - service = MyService(node) - - # when - service.create_files() - - # then - file_path = Path(MyService.files[0]) - node.create_file.assert_called_with(file_path, TEMPLATE_TEXT) - - def test_run_startup(self): - # given - node = mock.MagicMock() - wait = True - service = MyService(node) - - # when - service.run_startup(wait=wait) - - # then - node.cmd.assert_called_with(MyService.startup[0], wait=wait) - - def test_run_startup_exception(self): - # given - node = mock.MagicMock() - node.cmd.side_effect = CoreCommandError(1, "error") - service = MyService(node) - - # when - with pytest.raises(ConfigServiceBootError): - service.run_startup(wait=True) - - def test_shutdown(self): - # given - node = mock.MagicMock() - service = MyService(node) - - # when - service.stop() - - # then - node.cmd.assert_called_with(MyService.shutdown[0]) - - def test_run_validation(self): - # given - node = mock.MagicMock() - service = MyService(node) - - # when - service.run_validation() - - # then - node.cmd.assert_called_with(MyService.validate[0]) - - def test_run_validation_timer(self): - # given - node = mock.MagicMock() - service = MyService(node) - service.validation_mode = ConfigServiceMode.TIMER - service.validation_timer = 0 - - # when - service.run_validation() - - # then - node.cmd.assert_called_with(MyService.validate[0]) - - def test_run_validation_timer_exception(self): - # given - node = mock.MagicMock() - node.cmd.side_effect = CoreCommandError(1, "error") - service = MyService(node) - service.validation_mode = ConfigServiceMode.TIMER - service.validation_period = 0 - service.validation_timer = 0 - - # when - with pytest.raises(ConfigServiceBootError): - 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_period = 0 - service.validation_timer = 0 - - # when - service.run_validation() - - # then - node.cmd.assert_called_with(MyService.validate[0]) - - def test_run_validation_non_blocking_exception(self): - # given - node = mock.MagicMock() - node.cmd.side_effect = CoreCommandError(1, "error") - service = MyService(node) - service.validation_mode = ConfigServiceMode.NON_BLOCKING - service.validation_period = 0 - service.validation_timer = 0 - - # when - with pytest.raises(ConfigServiceBootError): - service.run_validation() - - def test_render_config(self): - # given - node = mock.MagicMock() - service = MyService(node) - - # when - config = service.render_config() - - # then - assert config == {"value1": "", "value2": "", "value3": ""} - - def test_render_config_custom(self): - # given - node = mock.MagicMock() - service = MyService(node) - custom_config = {"value1": "1", "value2": "2", "value3": "3"} - service.set_config(custom_config) - - # when - config = service.render_config() - - # then - assert config == custom_config - - def test_set_config(self): - # given - node = mock.MagicMock() - service = MyService(node) - custom_config = {"value1": "1", "value2": "2", "value3": "3"} - - # when - service.set_config(custom_config) - - # then - assert service.custom_config == custom_config - - def test_set_config_exception(self): - # given - node = mock.MagicMock() - service = MyService(node) - custom_config = {"value4": "1"} - - # when - with pytest.raises(CoreError): - service.set_config(custom_config) - - def test_start_blocking(self): - # given - node = mock.MagicMock() - service = MyService(node) - service.create_dirs = mock.MagicMock() - service.create_files = mock.MagicMock() - service.run_startup = mock.MagicMock() - service.run_validation = mock.MagicMock() - service.wait_validation = mock.MagicMock() - - # when - service.start() - - # then - service.create_files.assert_called_once() - service.create_dirs.assert_called_once() - service.run_startup.assert_called_once() - service.run_validation.assert_not_called() - service.wait_validation.assert_not_called() - - def test_start_timer(self): - # given - node = mock.MagicMock() - service = MyService(node) - service.validation_mode = ConfigServiceMode.TIMER - service.create_dirs = mock.MagicMock() - service.create_files = mock.MagicMock() - service.run_startup = mock.MagicMock() - service.run_validation = mock.MagicMock() - service.wait_validation = mock.MagicMock() - - # when - service.start() - - # then - service.create_files.assert_called_once() - service.create_dirs.assert_called_once() - service.run_startup.assert_called_once() - service.run_validation.assert_not_called() - service.wait_validation.assert_called_once() - - def test_start_non_blocking(self): - # given - node = mock.MagicMock() - service = MyService(node) - service.validation_mode = ConfigServiceMode.NON_BLOCKING - service.create_dirs = mock.MagicMock() - service.create_files = mock.MagicMock() - service.run_startup = mock.MagicMock() - service.run_validation = mock.MagicMock() - service.wait_validation = mock.MagicMock() - - # when - service.start() - - # then - service.create_files.assert_called_once() - service.create_dirs.assert_called_once() - service.run_startup.assert_called_once() - service.run_validation.assert_called_once() - service.wait_validation.assert_not_called() diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 250cbfc5b..ef5bcaf2a 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -24,11 +24,10 @@ MobilityAction, MoveNodesRequest, Node, - NodeServiceData, NodeType, Position, ServiceAction, - ServiceValidationMode, + ServiceData, SessionLocation, SessionState, ) @@ -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 @@ -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 = ServiceData( + 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.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.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,80 +611,7 @@ 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() session = grpc_server.coreemu.create_session() @@ -710,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_services.py b/daemon/tests/test_services.py index 69234e3a7..a7fe987e3 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -1,376 +1,296 @@ -import itertools from pathlib import Path +from unittest import mock 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" +from core.config import ConfigBool, ConfigString +from core.errors import CoreCommandError, CoreError +from core.services.base import Service, ServiceBootError, ServiceMode + +TEMPLATE_TEXT = "echo hello" + + +class MyService(Service): + name = "MyService" + group = "MyGroup" + directories = ["/usr/local/lib"] + files = ["test.sh"] + executables = [] + dependencies = [] + startup = [f"sh {files[0]}"] + validate = [f"pidof {files[0]}"] + shutdown = [f"pkill {files[0]}"] + validation_mode = ServiceMode.BLOCKING + default_configs = [ + ConfigString(id="value1", label="Text"), + ConfigBool(id="value2", label="Boolean"), + ConfigString( + id="value3", label="Multiple Choice", options=["value1", "value2", "value3"] + ), + ] + modes = { + "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, + "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, + "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, + } + + def get_text_template(self, name: str) -> str: + return TEMPLATE_TEXT class TestServices: - def test_service_all_files(self, session: Session): + def test_set_template(self): # given - ServiceManager.add_services(_SERVICES_PATH) - file_name = "myservice.sh" - node = session.add_node(CoreNode) + node = mock.MagicMock() + text = "echo custom" + service = MyService(node) # when - session.services.set_service_file(node.id, SERVICE_ONE, file_name, "# test") + service.set_template(MyService.files[0], text) # 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 + assert MyService.files[0] in service.custom_templates + assert service.custom_templates[MyService.files[0]] == text - def test_service_all_configs(self, session: Session): + def test_create_directories(self): # given - ServiceManager.add_services(_SERVICES_PATH) - node = session.add_node(CoreNode) + node = mock.MagicMock() + service = MyService(node) # when - session.services.set_service(node.id, SERVICE_ONE) - session.services.set_service(node.id, SERVICE_TWO) + service.create_dirs() # then - all_configs = session.services.all_configs() - assert all_configs - assert len(all_configs) == 2 + directory = Path(MyService.directories[0]) + node.create_dir.assert_called_with(directory) - def test_service_add_services(self, session: Session): + def test_create_files_custom(self): # given - ServiceManager.add_services(_SERVICES_PATH) - node = session.add_node(CoreNode) - total_service = len(node.services) + node = mock.MagicMock() + service = MyService(node) + text = "echo custom" + service.set_template(MyService.files[0], text) # when - session.services.add_services(node, node.model, [SERVICE_ONE, SERVICE_TWO]) + service.create_files() # then - assert node.services - assert len(node.services) == total_service + 2 + file_path = Path(MyService.files[0]) + node.create_file.assert_called_with(file_path, text) - def test_service_file(self, request, session: Session): + def test_create_files_text(self): # 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) + node = mock.MagicMock() + service = MyService(node) # when - session.services.create_service_files(node, my_service) + service.create_files() # then - if not request.config.getoption("mock"): - assert file_path.exists() + file_path = Path(MyService.files[0]) + node.create_file.assert_called_with(file_path, TEMPLATE_TEXT) - def test_service_validate(self, session: Session): + def test_run_startup(self): # 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) + node = mock.MagicMock() + wait = True + service = MyService(node) # when - status = session.services.validate_service(node, my_service) + service.run_startup(wait=wait) # then - assert not status + node.cmd.assert_called_with(MyService.startup[0], wait=wait) - def test_service_validate_error(self, session: Session): + def test_run_startup_exception(self): # 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")) + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) # when - status = session.services.validate_service(node, my_service) - - # then - assert status + with pytest.raises(ServiceBootError): + service.run_startup(wait=True) - def test_service_startup(self, session: Session): + def test_shutdown(self): # 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) + node = mock.MagicMock() + service = MyService(node) # when - status = session.services.startup_service(node, my_service, wait=True) + service.stop() # then - assert not status + node.cmd.assert_called_with(MyService.shutdown[0]) - def test_service_startup_error(self, session: Session): + def test_run_validation(self): # 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")) + node = mock.MagicMock() + service = MyService(node) # when - status = session.services.startup_service(node, my_service, wait=True) + service.run_validation() # then - assert status + node.cmd.assert_called_with(MyService.validate[0]) - def test_service_stop(self, session: Session): + def test_run_validation_timer(self): # 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) + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ServiceMode.TIMER + service.validation_timer = 0 # when - status = session.services.stop_service(node, my_service) + service.run_validation() # then - assert not status + node.cmd.assert_called_with(MyService.validate[0]) + + def test_run_validation_timer_exception(self): + # given + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) + service.validation_mode = ServiceMode.TIMER + service.validation_period = 0 + service.validation_timer = 0 + + # when + with pytest.raises(ServiceBootError): + service.run_validation() - def test_service_stop_error(self, session: Session): + def test_run_validation_non_blocking(self): # 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")) + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ServiceMode.NON_BLOCKING + service.validation_period = 0 + service.validation_timer = 0 # when - status = session.services.stop_service(node, my_service) + service.run_validation() # then - assert status + node.cmd.assert_called_with(MyService.validate[0]) - def test_service_custom_startup(self, session: Session): + def test_run_validation_non_blocking_exception(self): # given - ServiceManager.add_services(_SERVICES_PATH) - my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node(CoreNode) + node = mock.MagicMock() + node.cmd.side_effect = CoreCommandError(1, "error") + service = MyService(node) + service.validation_mode = ServiceMode.NON_BLOCKING + service.validation_period = 0 + service.validation_timer = 0 # 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",) + with pytest.raises(ServiceBootError): + service.run_validation() + + def test_render_config(self): + # given + node = mock.MagicMock() + service = MyService(node) + + # when + config = service.render_config() # then - assert my_service.startup != custom_my_service.startup + assert config == {"value1": "", "value2": "", "value3": ""} - def test_service_set_file(self, session: Session): + def test_render_config_custom(self): # 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 - ) + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value1": "1", "value2": "2", "value3": "3"} + service.set_config(custom_config) # 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): + config = service.render_config() + + # then + assert config == custom_config + + def test_set_config(self): # given - ServiceManager.add_services(_SERVICES_PATH) - my_service = ServiceManager.get(SERVICE_ONE) - node = session.add_node(CoreNode) + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value1": "1", "value2": "2", "value3": "3"} # 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 - ) + service.set_config(custom_config) # then - assert no_service is None - assert default_service == my_service - assert custom_service and custom_service != my_service + assert service.custom_config == custom_config - def test_services_dependency(self): + def test_set_config_exception(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] + node = mock.MagicMock() + service = MyService(node) + custom_config = {"value4": "1"} # 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): + with pytest.raises(CoreError): + service.set_config(custom_config) + + def test_start_blocking(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): + node = mock.MagicMock() + service = MyService(node) + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() + + # when + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_not_called() + service.wait_validation.assert_not_called() + + def test_start_timer(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} + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ServiceMode.TIMER + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() # 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): + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_not_called() + service.wait_validation.assert_called_once() + + def test_start_non_blocking(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} + node = mock.MagicMock() + service = MyService(node) + service.validation_mode = ServiceMode.NON_BLOCKING + service.create_dirs = mock.MagicMock() + service.create_files = mock.MagicMock() + service.run_startup = mock.MagicMock() + service.run_validation = mock.MagicMock() + service.wait_validation = mock.MagicMock() # 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 + service.start() + + # then + service.create_files.assert_called_once() + service.create_dirs.assert_called_once() + service.run_startup.assert_called_once() + service.run_validation.assert_called_once() + service.wait_validation.assert_not_called() diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 6c2e45a6f..01e5f6eb5 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -11,7 +11,7 @@ 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 +from core.services.defaults.utilservices.services import DefaultRouteService 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.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.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 diff --git a/docs/configservices.md b/docs/configservices.md deleted file mode 100644 index a82c8c59c..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.configservice.base import ConfigService, ConfigServiceMode, ShadowDir - - -# class that subclasses ConfigService -class ExampleService(ConfigService): - # 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: ConfigServiceMode = ConfigServiceMode.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/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/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./.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//.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//.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 = + custom_services_dir = ``` 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//.coregui/custom_services -#custom_config_services_dir = /home//.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) 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] = [] 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 @@ - + - + - + - + - + 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/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 @@ - + - + - + - + - + @@ -34,9 +34,6 @@ - - - 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 @@ - + @@ -8,27 +8,27 @@ - + - + - + - + - + - + @@ -42,17 +42,6 @@ - - - - - - - - - - - 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 @@ - + @@ -8,27 +8,27 @@ - + - + - + - + - + - + @@ -62,17 +62,6 @@ - - - - - - - - - - - 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 @@ - + @@ -8,9 +8,9 @@ - + - + @@ -25,9 +25,6 @@ - - - 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 @@ - + @@ -18,27 +18,27 @@ - + - + - + - + - + - + @@ -52,17 +52,6 @@ - - - - - - - - - - - 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 @@ - + @@ -8,19 +8,19 @@ - + - + - + - + @@ -31,14 +31,6 @@ - - - - - - - - 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) 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 @@ - + @@ -8,20 +8,20 @@ - + - + - + - + @@ -32,14 +32,6 @@ - - - - - - - -