Merge pull request #357 from coreemu/feature/config-service
Feature/config service
This commit is contained in:
commit
c5666f9112
81 changed files with 5044 additions and 187 deletions
|
@ -11,7 +11,21 @@ import grpc
|
|||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import core_pb2, core_pb2_grpc
|
||||
from core.api.grpc import configservices_pb2, core_pb2, core_pb2_grpc
|
||||
from core.api.grpc.configservices_pb2 import (
|
||||
GetConfigServiceDefaultsRequest,
|
||||
GetConfigServiceDefaultsResponse,
|
||||
GetConfigServicesRequest,
|
||||
GetConfigServicesResponse,
|
||||
GetNodeConfigServiceConfigsRequest,
|
||||
GetNodeConfigServiceConfigsResponse,
|
||||
GetNodeConfigServiceRequest,
|
||||
GetNodeConfigServiceResponse,
|
||||
GetNodeConfigServicesRequest,
|
||||
GetNodeConfigServicesResponse,
|
||||
SetNodeConfigServiceRequest,
|
||||
SetNodeConfigServiceResponse,
|
||||
)
|
||||
|
||||
|
||||
class InterfaceHelper:
|
||||
|
@ -163,6 +177,7 @@ class CoreGrpcClient:
|
|||
service_configs: List[core_pb2.ServiceConfig] = None,
|
||||
service_file_configs: List[core_pb2.ServiceFileConfig] = None,
|
||||
asymmetric_links: List[core_pb2.Link] = None,
|
||||
config_service_configs: List[configservices_pb2.ConfigServiceConfig] = None,
|
||||
) -> core_pb2.StartSessionResponse:
|
||||
"""
|
||||
Start a session.
|
||||
|
@ -179,6 +194,7 @@ class CoreGrpcClient:
|
|||
:param service_configs: node service configurations
|
||||
:param service_file_configs: node service file configurations
|
||||
:param asymmetric_links: asymmetric links to edit
|
||||
:param config_service_configs: config service configurations
|
||||
:return: start session response
|
||||
"""
|
||||
request = core_pb2.StartSessionRequest(
|
||||
|
@ -194,6 +210,7 @@ class CoreGrpcClient:
|
|||
service_configs=service_configs,
|
||||
service_file_configs=service_file_configs,
|
||||
asymmetric_links=asymmetric_links,
|
||||
config_service_configs=config_service_configs,
|
||||
)
|
||||
return self.stub.StartSession(request)
|
||||
|
||||
|
@ -1078,6 +1095,44 @@ class CoreGrpcClient:
|
|||
request = core_pb2.GetInterfacesRequest()
|
||||
return self.stub.GetInterfaces(request)
|
||||
|
||||
def get_config_services(self) -> GetConfigServicesResponse:
|
||||
request = GetConfigServicesRequest()
|
||||
return self.stub.GetConfigServices(request)
|
||||
|
||||
def get_config_service_defaults(
|
||||
self, name: str
|
||||
) -> GetConfigServiceDefaultsResponse:
|
||||
request = GetConfigServiceDefaultsRequest(name=name)
|
||||
return self.stub.GetConfigServiceDefaults(request)
|
||||
|
||||
def get_node_config_service_configs(
|
||||
self, session_id: int
|
||||
) -> GetNodeConfigServiceConfigsResponse:
|
||||
request = GetNodeConfigServiceConfigsRequest(session_id=session_id)
|
||||
return self.stub.GetNodeConfigServiceConfigs(request)
|
||||
|
||||
def get_node_config_service(
|
||||
self, session_id: int, node_id: int, name: str
|
||||
) -> GetNodeConfigServiceResponse:
|
||||
request = GetNodeConfigServiceRequest(
|
||||
session_id=session_id, node_id=node_id, name=name
|
||||
)
|
||||
return self.stub.GetNodeConfigService(request)
|
||||
|
||||
def get_node_config_services(
|
||||
self, session_id: int, node_id: int
|
||||
) -> GetNodeConfigServicesResponse:
|
||||
request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id)
|
||||
return self.stub.GetNodeConfigServices(request)
|
||||
|
||||
def set_node_config_service(
|
||||
self, session_id: int, node_id: int, name: str, config: Dict[str, str]
|
||||
) -> SetNodeConfigServiceResponse:
|
||||
request = SetNodeConfigServiceRequest(
|
||||
session_id=session_id, node_id=node_id, name=name, config=config
|
||||
)
|
||||
return self.stub.SetNodeConfigService(request)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""
|
||||
Open connection to server, must be closed manually.
|
||||
|
|
|
@ -3,7 +3,7 @@ import time
|
|||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
from core import utils
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.config import ConfigurableOptions
|
||||
from core.emulator.data import LinkData
|
||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
||||
|
@ -33,6 +33,7 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
|
|||
options.opaque = node_proto.opaque
|
||||
options.image = node_proto.image
|
||||
options.services = node_proto.services
|
||||
options.config_services = node_proto.config_services
|
||||
if node_proto.emane:
|
||||
options.emane = node_proto.emane
|
||||
if node_proto.server:
|
||||
|
@ -190,7 +191,7 @@ def convert_value(value: Any) -> str:
|
|||
|
||||
def get_config_options(
|
||||
config: Dict[str, str], configurable_options: Type[ConfigurableOptions]
|
||||
) -> Dict[str, core_pb2.ConfigOption]:
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
"""
|
||||
Retrieve configuration options in a form that is used by the grpc server.
|
||||
|
||||
|
@ -201,7 +202,7 @@ def get_config_options(
|
|||
results = {}
|
||||
for configuration in configurable_options.configurations():
|
||||
value = config[configuration.id]
|
||||
config_option = core_pb2.ConfigOption(
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=configuration.label,
|
||||
name=configuration.id,
|
||||
value=value,
|
||||
|
|
|
@ -5,11 +5,33 @@ import re
|
|||
import tempfile
|
||||
import time
|
||||
from concurrent import futures
|
||||
from typing import Type
|
||||
|
||||
import grpc
|
||||
from grpc import ServicerContext
|
||||
|
||||
from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils
|
||||
from core.api.grpc import (
|
||||
common_pb2,
|
||||
configservices_pb2,
|
||||
core_pb2,
|
||||
core_pb2_grpc,
|
||||
grpcutils,
|
||||
)
|
||||
from core.api.grpc.configservices_pb2 import (
|
||||
ConfigService,
|
||||
GetConfigServiceDefaultsRequest,
|
||||
GetConfigServiceDefaultsResponse,
|
||||
GetConfigServicesRequest,
|
||||
GetConfigServicesResponse,
|
||||
GetNodeConfigServiceConfigsRequest,
|
||||
GetNodeConfigServiceConfigsResponse,
|
||||
GetNodeConfigServiceRequest,
|
||||
GetNodeConfigServiceResponse,
|
||||
GetNodeConfigServicesRequest,
|
||||
GetNodeConfigServicesResponse,
|
||||
SetNodeConfigServiceRequest,
|
||||
SetNodeConfigServiceResponse,
|
||||
)
|
||||
from core.api.grpc.events import EventStreamer
|
||||
from core.api.grpc.grpcutils import (
|
||||
get_config_options,
|
||||
|
@ -25,7 +47,7 @@ from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
|
|||
from core.emulator.session import Session
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
|
||||
from core.nodes.base import NodeBase
|
||||
from core.nodes.base import CoreNodeBase, NodeBase
|
||||
from core.nodes.docker import DockerNode
|
||||
from core.nodes.lxd import LxcNode
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
@ -79,6 +101,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
:param context:
|
||||
:return: session object that satisfies, if session not found then raise an
|
||||
exception
|
||||
:raises Exception: raises grpc exception when session does not exist
|
||||
"""
|
||||
session = self.coreemu.sessions.get(session_id)
|
||||
if not session:
|
||||
|
@ -95,12 +118,29 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
:param node_id: node id
|
||||
:param context:
|
||||
:return: node object that satisfies. If node not found then raise an exception.
|
||||
:raises Exception: raises grpc exception when node does not exist
|
||||
"""
|
||||
try:
|
||||
return session.get_node(node_id)
|
||||
except CoreError:
|
||||
context.abort(grpc.StatusCode.NOT_FOUND, f"node {node_id} not found")
|
||||
|
||||
def validate_service(
|
||||
self, name: str, context: ServicerContext
|
||||
) -> Type[ConfigService]:
|
||||
"""
|
||||
Validates a configuration service is a valid known service.
|
||||
|
||||
:param name: name of service to validate
|
||||
:param context: grpc context
|
||||
:return: class for service to validate
|
||||
:raises Exception: raises grpc exception when service does not exist
|
||||
"""
|
||||
service = self.coreemu.service_manager.services.get(name)
|
||||
if not service:
|
||||
context.abort(grpc.StatusCode.NOT_FOUND, f"unknown service {name}")
|
||||
return service
|
||||
|
||||
def StartSession(
|
||||
self, request: core_pb2.StartSessionRequest, context: ServicerContext
|
||||
) -> core_pb2.StartSessionResponse:
|
||||
|
@ -108,7 +148,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
Start a session.
|
||||
|
||||
:param request: start session request
|
||||
:param context: grcp context
|
||||
:param context: grpc context
|
||||
:return: start session response
|
||||
"""
|
||||
logging.debug("start session: %s", request)
|
||||
|
@ -157,6 +197,15 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
for config in request.service_configs:
|
||||
grpcutils.service_configuration(session, config)
|
||||
|
||||
# config service configs
|
||||
for config in request.config_service_configs:
|
||||
node = self.get_node(session, config.node_id, context)
|
||||
service = node.config_services[config.name]
|
||||
if config.config:
|
||||
service.set_config(config.config)
|
||||
for name, template in config.templates.items():
|
||||
service.set_template(name, template)
|
||||
|
||||
# service file configs
|
||||
for config in request.service_file_configs:
|
||||
session.services.set_service_file(
|
||||
|
@ -196,7 +245,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
Stop a running session.
|
||||
|
||||
:param request: stop session request
|
||||
:param context: grcp context
|
||||
:param context: grpc context
|
||||
:return: stop session response
|
||||
"""
|
||||
logging.debug("stop session: %s", request)
|
||||
|
@ -426,6 +475,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
if services is None:
|
||||
services = []
|
||||
services = [x.name for x in services]
|
||||
config_services = getattr(node, "config_services", {})
|
||||
config_services = [x for x in config_services]
|
||||
emane_model = None
|
||||
if isinstance(node, EmaneNet):
|
||||
emane_model = node.model.name
|
||||
|
@ -441,6 +492,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
services=services,
|
||||
icon=node.icon,
|
||||
image=image,
|
||||
config_services=config_services,
|
||||
)
|
||||
if isinstance(node, (DockerNode, LxcNode)):
|
||||
node_proto.image = node.image
|
||||
|
@ -1429,3 +1481,152 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
|
|||
return core_pb2.EmaneLinkResponse(result=True)
|
||||
else:
|
||||
return core_pb2.EmaneLinkResponse(result=False)
|
||||
|
||||
def GetConfigServices(
|
||||
self, request: GetConfigServicesRequest, context: ServicerContext
|
||||
) -> GetConfigServicesResponse:
|
||||
"""
|
||||
Gets all currently known configuration services.
|
||||
|
||||
:param request: get config services request
|
||||
:param context: grpc context
|
||||
:return: get config services response
|
||||
"""
|
||||
services = []
|
||||
for service in self.coreemu.service_manager.services.values():
|
||||
service_proto = ConfigService(
|
||||
name=service.name,
|
||||
group=service.group,
|
||||
executables=service.executables,
|
||||
dependencies=service.dependencies,
|
||||
directories=service.directories,
|
||||
files=service.files,
|
||||
startup=service.startup,
|
||||
validate=service.validate,
|
||||
shutdown=service.shutdown,
|
||||
validation_mode=service.validation_mode.value,
|
||||
validation_timer=service.validation_timer,
|
||||
validation_period=service.validation_period,
|
||||
)
|
||||
services.append(service_proto)
|
||||
return GetConfigServicesResponse(services=services)
|
||||
|
||||
def GetNodeConfigService(
|
||||
self, request: GetNodeConfigServiceRequest, context: ServicerContext
|
||||
) -> GetNodeConfigServiceResponse:
|
||||
"""
|
||||
Gets configuration, for a given configuration service, for a given node.
|
||||
|
||||
:param request: get node config service request
|
||||
:param context: grpc context
|
||||
:return: get node config service response
|
||||
"""
|
||||
session = self.get_session(request.session_id, context)
|
||||
node = self.get_node(session, request.node_id, context)
|
||||
self.validate_service(request.name, context)
|
||||
service = node.config_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)
|
||||
|
||||
def GetConfigServiceDefaults(
|
||||
self, request: GetConfigServiceDefaultsRequest, context: ServicerContext
|
||||
) -> GetConfigServiceDefaultsResponse:
|
||||
"""
|
||||
Get default values for a given configuration service.
|
||||
|
||||
:param request: get config service defaults request
|
||||
:param context: grpc context
|
||||
:return: get config service defaults response
|
||||
"""
|
||||
service_class = self.validate_service(request.name, context)
|
||||
service = service_class(None)
|
||||
templates = service.get_templates()
|
||||
config = {}
|
||||
for configuration in service.default_configs:
|
||||
config_option = common_pb2.ConfigOption(
|
||||
label=configuration.label,
|
||||
name=configuration.id,
|
||||
value=configuration.default,
|
||||
type=configuration.type.value,
|
||||
select=configuration.options,
|
||||
group="Settings",
|
||||
)
|
||||
config[configuration.id] = config_option
|
||||
modes = []
|
||||
for name, mode_config in service.modes.items():
|
||||
mode = configservices_pb2.ConfigMode(name=name, config=mode_config)
|
||||
modes.append(mode)
|
||||
return GetConfigServiceDefaultsResponse(
|
||||
templates=templates, config=config, modes=modes
|
||||
)
|
||||
|
||||
def GetNodeConfigServiceConfigs(
|
||||
self, request: GetNodeConfigServiceConfigsRequest, context: ServicerContext
|
||||
) -> GetNodeConfigServiceConfigsResponse:
|
||||
"""
|
||||
Get current custom templates and config for configuration services for a given
|
||||
node.
|
||||
|
||||
:param request: get node config service configs request
|
||||
:param context: grpc context
|
||||
:return: get node config service configs response
|
||||
"""
|
||||
session = self.get_session(request.session_id, context)
|
||||
configs = []
|
||||
for node in session.nodes.values():
|
||||
if not isinstance(node, CoreNodeBase):
|
||||
continue
|
||||
|
||||
for name, service in node.config_services.items():
|
||||
if not service.custom_templates and not service.custom_config:
|
||||
continue
|
||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
||||
node_id=node.id,
|
||||
name=name,
|
||||
templates=service.custom_templates,
|
||||
config=service.custom_config,
|
||||
)
|
||||
configs.append(config_proto)
|
||||
return GetNodeConfigServiceConfigsResponse(configs=configs)
|
||||
|
||||
def GetNodeConfigServices(
|
||||
self, request: GetNodeConfigServicesRequest, context: ServicerContext
|
||||
) -> GetNodeConfigServicesResponse:
|
||||
"""
|
||||
Get configuration services for a given node.
|
||||
|
||||
:param request: get node config services request
|
||||
:param context: grpc context
|
||||
:return: get node config services response
|
||||
"""
|
||||
session = self.get_session(request.session_id, context)
|
||||
node = self.get_node(session, request.node_id, context)
|
||||
services = node.config_services.keys()
|
||||
return GetNodeConfigServicesResponse(services=services)
|
||||
|
||||
def SetNodeConfigService(
|
||||
self, request: SetNodeConfigServiceRequest, context: ServicerContext
|
||||
) -> SetNodeConfigServiceResponse:
|
||||
"""
|
||||
Set custom config, for a given configuration service, for a given node.
|
||||
|
||||
:param request: set node config service request
|
||||
:param context: grpc context
|
||||
:return: set node config service response
|
||||
"""
|
||||
session = self.get_session(request.session_id, context)
|
||||
node = self.get_node(session, request.node_id, context)
|
||||
self.validate_service(request.name, context)
|
||||
service = node.config_services.get(request.name)
|
||||
if service:
|
||||
service.set_config(request.config)
|
||||
return SetNodeConfigServiceResponse(result=True)
|
||||
else:
|
||||
context.abort(
|
||||
grpc.StatusCode.NOT_FOUND,
|
||||
f"node {node.name} missing service {request.name}",
|
||||
)
|
||||
|
|
0
daemon/core/configservice/__init__.py
Normal file
0
daemon/core/configservice/__init__.py
Normal file
395
daemon/core/configservice/base.py
Normal file
395
daemon/core/configservice/base.py
Normal file
|
@ -0,0 +1,395 @@
|
|||
import abc
|
||||
import enum
|
||||
import inspect
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from mako import exceptions
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.template import Template
|
||||
|
||||
from core.config import Configuration
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes.base import CoreNode
|
||||
|
||||
TEMPLATES_DIR = "templates"
|
||||
|
||||
|
||||
class ConfigServiceMode(enum.Enum):
|
||||
BLOCKING = 0
|
||||
NON_BLOCKING = 1
|
||||
TIMER = 2
|
||||
|
||||
|
||||
class ConfigServiceBootError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigService(abc.ABC):
|
||||
"""
|
||||
Base class for creating configurable services.
|
||||
"""
|
||||
|
||||
# validation period in seconds, how frequent validation is attempted
|
||||
validation_period = 0.5
|
||||
|
||||
# time to wait in seconds for determining if service started successfully
|
||||
validation_timer = 5
|
||||
|
||||
def __init__(self, node: CoreNode) -> None:
|
||||
"""
|
||||
Create ConfigService instance.
|
||||
|
||||
:param node: node this service is assigned to
|
||||
"""
|
||||
self.node = node
|
||||
class_file = inspect.getfile(self.__class__)
|
||||
templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR)
|
||||
self.templates = TemplateLookup(directories=templates_path)
|
||||
self.config = {}
|
||||
self.custom_templates = {}
|
||||
self.custom_config = {}
|
||||
configs = self.default_configs[:]
|
||||
self._define_config(configs)
|
||||
|
||||
@staticmethod
|
||||
def clean_text(text: str) -> str:
|
||||
"""
|
||||
Returns space stripped text for string literals, while keeping space
|
||||
indentations.
|
||||
|
||||
:param text: text to clean
|
||||
:return: cleaned text
|
||||
"""
|
||||
return inspect.cleandoc(text)
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def group(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def directories(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def files(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def default_configs(self) -> List[Configuration]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def modes(self) -> Dict[str, Dict[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def executables(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def dependencies(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def startup(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validate(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def shutdown(self) -> List[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def validation_mode(self) -> ConfigServiceMode:
|
||||
raise NotImplementedError
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Creates services files/directories, runs startup, and validates based on
|
||||
validation mode.
|
||||
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: when there is an error starting service
|
||||
"""
|
||||
logging.info("node(%s) service(%s) starting...", self.node.name, self.name)
|
||||
self.create_dirs()
|
||||
self.create_files()
|
||||
wait = self.validation_mode == ConfigServiceMode.BLOCKING
|
||||
self.run_startup(wait)
|
||||
if not wait:
|
||||
if self.validation_mode == ConfigServiceMode.TIMER:
|
||||
self.wait_validation()
|
||||
else:
|
||||
self.run_validation()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Stop service using shutdown commands.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
for cmd in self.shutdown:
|
||||
try:
|
||||
self.node.cmd(cmd)
|
||||
except CoreCommandError:
|
||||
logging.exception(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failed shutdown: {cmd}"
|
||||
)
|
||||
|
||||
def restart(self) -> None:
|
||||
"""
|
||||
Restarts service by running stop and then start.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def create_dirs(self) -> None:
|
||||
"""
|
||||
Creates directories for service.
|
||||
|
||||
:return: nothing
|
||||
:raises CoreError: when there is a failure creating a directory
|
||||
"""
|
||||
for directory in self.directories:
|
||||
try:
|
||||
self.node.privatedir(directory)
|
||||
except (CoreCommandError, ValueError):
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"failure to create service directory: {directory}"
|
||||
)
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns key/value data, used when rendering file templates.
|
||||
|
||||
:return: key/value template data
|
||||
"""
|
||||
return {}
|
||||
|
||||
def set_template(self, name: str, template: str) -> None:
|
||||
"""
|
||||
Store custom template to render for a given file.
|
||||
|
||||
:param name: file to store custom template for
|
||||
:param template: custom template to render
|
||||
:return: nothing
|
||||
"""
|
||||
self.custom_templates[name] = template
|
||||
|
||||
def get_text_template(self, name: str) -> str:
|
||||
"""
|
||||
Retrieves text based template for files that do not have a file based template.
|
||||
|
||||
:param name: name of file to get template for
|
||||
:return: template to render
|
||||
"""
|
||||
raise CoreError(f"service({self.name}) unknown template({name})")
|
||||
|
||||
def get_templates(self) -> Dict[str, str]:
|
||||
"""
|
||||
Retrieves mapping of file names to templates for all cases, which
|
||||
includes custom templates, file templates, and text templates.
|
||||
|
||||
:return: mapping of files to templates
|
||||
"""
|
||||
templates = {}
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
template = self.custom_templates[name]
|
||||
template = self.clean_text(template)
|
||||
elif self.templates.has_template(basename):
|
||||
template = self.templates.get_template(basename).source
|
||||
else:
|
||||
template = self.get_text_template(name)
|
||||
template = self.clean_text(template)
|
||||
templates[name] = template
|
||||
return templates
|
||||
|
||||
def create_files(self) -> None:
|
||||
"""
|
||||
Creates service files inside associated node.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
data = self.data()
|
||||
for name in self.files:
|
||||
basename = pathlib.Path(name).name
|
||||
if name in self.custom_templates:
|
||||
text = self.custom_templates[name]
|
||||
rendered = self.render_text(text, data)
|
||||
elif self.templates.has_template(basename):
|
||||
rendered = self.render_template(basename, data)
|
||||
else:
|
||||
text = self.get_text_template(name)
|
||||
rendered = self.render_text(text, data)
|
||||
logging.debug(
|
||||
"node(%s) service(%s) template(%s): \n%s",
|
||||
self.node.name,
|
||||
self.name,
|
||||
name,
|
||||
rendered,
|
||||
)
|
||||
self.node.nodefile(name, rendered)
|
||||
|
||||
def run_startup(self, wait: bool) -> None:
|
||||
"""
|
||||
Run startup commands for service on node.
|
||||
|
||||
:param wait: wait successful command exit status when True, ignore status
|
||||
otherwise
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: when a command that waits fails
|
||||
"""
|
||||
for cmd in self.startup:
|
||||
try:
|
||||
self.node.cmd(cmd, wait=wait)
|
||||
except CoreCommandError as e:
|
||||
raise ConfigServiceBootError(
|
||||
f"node({self.node.name}) service({self.name}) failed startup: {e}"
|
||||
)
|
||||
|
||||
def wait_validation(self) -> None:
|
||||
"""
|
||||
Waits for a period of time to consider service started successfully.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
time.sleep(self.validation_timer)
|
||||
|
||||
def run_validation(self) -> None:
|
||||
"""
|
||||
Runs validation commands for service on node.
|
||||
|
||||
:return: nothing
|
||||
:raises ConfigServiceBootError: if there is a validation failure
|
||||
"""
|
||||
start = time.monotonic()
|
||||
cmds = self.validate[:]
|
||||
index = 0
|
||||
while cmds:
|
||||
cmd = cmds[index]
|
||||
try:
|
||||
self.node.cmd(cmd)
|
||||
del cmds[index]
|
||||
index += 1
|
||||
except CoreCommandError:
|
||||
logging.debug(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"validate command failed: {cmd}"
|
||||
)
|
||||
time.sleep(self.validation_period)
|
||||
|
||||
if cmds and time.monotonic() - start > self.validation_timer:
|
||||
raise ConfigServiceBootError(
|
||||
f"node({self.node.name}) service({self.name}) failed to validate"
|
||||
)
|
||||
|
||||
def _render(self, template: Template, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders template providing all associated data to template.
|
||||
|
||||
:param template: template to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
return template.render_unicode(
|
||||
node=self.node, config=self.render_config(), **data
|
||||
)
|
||||
|
||||
def render_text(self, text: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders text based template providing all associated data to template.
|
||||
|
||||
:param text: text to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
text = self.clean_text(text)
|
||||
try:
|
||||
template = Template(text)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_unicode()}"
|
||||
)
|
||||
|
||||
def render_template(self, basename: str, data: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Renders file based template providing all associated data to template.
|
||||
|
||||
:param basename: base name for file to render
|
||||
:param data: service specific defined data for template
|
||||
:return: rendered template
|
||||
"""
|
||||
try:
|
||||
template = self.templates.get_template(basename)
|
||||
return self._render(template, data)
|
||||
except Exception:
|
||||
raise CoreError(
|
||||
f"node({self.node.name}) service({self.name}) "
|
||||
f"{exceptions.text_error_template().render_template()}"
|
||||
)
|
||||
|
||||
def _define_config(self, configs: List[Configuration]) -> None:
|
||||
"""
|
||||
Initializes default configuration data.
|
||||
|
||||
:param configs: configs to initialize
|
||||
:return: nothing
|
||||
"""
|
||||
for config in configs:
|
||||
self.config[config.id] = config
|
||||
|
||||
def render_config(self) -> Dict[str, str]:
|
||||
"""
|
||||
Returns configuration data key/value pairs for rendering a template.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
if self.custom_config:
|
||||
return self.custom_config
|
||||
else:
|
||||
return {k: v.default for k, v in self.config.items()}
|
||||
|
||||
def set_config(self, data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Set configuration data from key/value pairs.
|
||||
|
||||
:param data: configuration key/values to set
|
||||
:return: nothing
|
||||
:raise CoreError: when an unknown configuration value is given
|
||||
"""
|
||||
for key, value in data.items():
|
||||
if key not in self.config:
|
||||
raise CoreError(f"unknown config: {key}")
|
||||
self.custom_config[key] = value
|
123
daemon/core/configservice/dependencies.py
Normal file
123
daemon/core/configservice/dependencies.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.configservice.base import ConfigService
|
||||
|
||||
|
||||
class ConfigServiceDependencies:
|
||||
"""
|
||||
Generates sets of services to start in order of their dependencies.
|
||||
"""
|
||||
|
||||
def __init__(self, services: Dict[str, "ConfigService"]) -> None:
|
||||
"""
|
||||
Create a ConfigServiceDependencies instance.
|
||||
|
||||
:param services: services for determining dependency sets
|
||||
"""
|
||||
# helpers to check validity
|
||||
self.dependents = {}
|
||||
self.started = set()
|
||||
self.node_services = {}
|
||||
for service in services.values():
|
||||
self.node_services[service.name] = service
|
||||
for dependency in service.dependencies:
|
||||
dependents = self.dependents.setdefault(dependency, set())
|
||||
dependents.add(service.name)
|
||||
|
||||
# used to find paths
|
||||
self.path = []
|
||||
self.visited = set()
|
||||
self.visiting = set()
|
||||
|
||||
def startup_paths(self) -> List[List["ConfigService"]]:
|
||||
"""
|
||||
Find startup path sets based on service dependencies.
|
||||
|
||||
:return: lists of lists of services that can be started in parallel
|
||||
"""
|
||||
paths = []
|
||||
for name in self.node_services:
|
||||
service = self.node_services[name]
|
||||
if service.name in self.started:
|
||||
logging.debug(
|
||||
"skipping service that will already be started: %s", service.name
|
||||
)
|
||||
continue
|
||||
|
||||
path = self._start(service)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
if self.started != set(self.node_services):
|
||||
raise ValueError(
|
||||
"failure to start all services: %s != %s"
|
||||
% (self.started, self.node_services.keys())
|
||||
)
|
||||
|
||||
return paths
|
||||
|
||||
def _reset(self) -> None:
|
||||
"""
|
||||
Clear out metadata used for finding service dependency sets.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
self.path = []
|
||||
self.visited.clear()
|
||||
self.visiting.clear()
|
||||
|
||||
def _start(self, service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Starts a oath for checking dependencies for a given service.
|
||||
|
||||
:param service: service to check dependencies for
|
||||
:return: list of config services to start in order
|
||||
"""
|
||||
logging.debug("starting service dependency check: %s", service.name)
|
||||
self._reset()
|
||||
return self._visit(service)
|
||||
|
||||
def _visit(self, current_service: "ConfigService") -> List["ConfigService"]:
|
||||
"""
|
||||
Visits a service when discovering dependency chains for service.
|
||||
|
||||
:param current_service: service being visited
|
||||
:return: list of dependent services for a visited service
|
||||
"""
|
||||
logging.debug("visiting service(%s): %s", current_service.name, self.path)
|
||||
self.visited.add(current_service.name)
|
||||
self.visiting.add(current_service.name)
|
||||
|
||||
# dive down
|
||||
for service_name in current_service.dependencies:
|
||||
if service_name not in self.node_services:
|
||||
raise ValueError(
|
||||
"required dependency was not included in node services: %s"
|
||||
% service_name
|
||||
)
|
||||
|
||||
if service_name in self.visiting:
|
||||
raise ValueError(
|
||||
"cyclic dependency at service(%s): %s"
|
||||
% (current_service.name, service_name)
|
||||
)
|
||||
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
# add service when bottom is found
|
||||
logging.debug("adding service to startup path: %s", current_service.name)
|
||||
self.started.add(current_service.name)
|
||||
self.path.append(current_service)
|
||||
self.visiting.remove(current_service.name)
|
||||
|
||||
# rise back up
|
||||
for service_name in self.dependents.get(current_service.name, []):
|
||||
if service_name not in self.visited:
|
||||
service = self.node_services[service_name]
|
||||
self._visit(service)
|
||||
|
||||
return self.path
|
82
daemon/core/configservice/manager.py
Normal file
82
daemon/core/configservice/manager.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
import logging
|
||||
import pathlib
|
||||
from typing import List, Type
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService
|
||||
from core.errors import CoreError
|
||||
|
||||
|
||||
class ConfigServiceManager:
|
||||
"""
|
||||
Manager for configurable services.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Create a ConfigServiceManager instance.
|
||||
"""
|
||||
self.services = {}
|
||||
|
||||
def get_service(self, name: str) -> Type[ConfigService]:
|
||||
"""
|
||||
Retrieve a service by name.
|
||||
|
||||
:param name: name of service
|
||||
:return: service class
|
||||
:raises CoreError: when service is not found
|
||||
"""
|
||||
service_class = self.services.get(name)
|
||||
if service_class is None:
|
||||
raise CoreError(f"service does not exit {name}")
|
||||
return service_class
|
||||
|
||||
def add(self, service: ConfigService) -> None:
|
||||
"""
|
||||
Add service to manager, checking service requirements have been met.
|
||||
|
||||
:param service: service to add to manager
|
||||
:return: nothing
|
||||
:raises CoreError: when service is a duplicate or has unmet executables
|
||||
"""
|
||||
name = service.name
|
||||
logging.debug("loading service: class(%s) name(%s)", service.__class__, name)
|
||||
|
||||
# avoid duplicate services
|
||||
if name in self.services:
|
||||
raise CoreError(f"duplicate service being added: {name}")
|
||||
|
||||
# validate dependent executables are present
|
||||
for executable in service.executables:
|
||||
try:
|
||||
utils.which(executable, required=True)
|
||||
except ValueError:
|
||||
raise CoreError(
|
||||
f"service({service.name}) missing executable {executable}"
|
||||
)
|
||||
|
||||
# make service available
|
||||
self.services[name] = service
|
||||
|
||||
def load(self, path: str) -> List[str]:
|
||||
"""
|
||||
Search path provided for configurable services and add them for being managed.
|
||||
|
||||
:param path: path to search configurable services
|
||||
:return: list errors when loading and adding services
|
||||
"""
|
||||
path = pathlib.Path(path)
|
||||
subdirs = [x for x in path.iterdir() if x.is_dir()]
|
||||
subdirs.append(path)
|
||||
service_errors = []
|
||||
for subdir in subdirs:
|
||||
logging.debug("loading config services from: %s", subdir)
|
||||
services = utils.load_classes(str(subdir), ConfigService)
|
||||
for service in services:
|
||||
logging.debug("found service: %s", service)
|
||||
try:
|
||||
self.add(service)
|
||||
except CoreError as e:
|
||||
service_errors.append(service.name)
|
||||
logging.debug("not loading service(%s): %s", service.name, e)
|
||||
return service_errors
|
0
daemon/core/configservices/__init__.py
Normal file
0
daemon/core/configservices/__init__.py
Normal file
0
daemon/core/configservices/frrservices/__init__.py
Normal file
0
daemon/core/configservices/frrservices/__init__.py
Normal file
391
daemon/core/configservices/frrservices/services.py
Normal file
391
daemon/core/configservices/frrservices/services.py
Normal file
|
@ -0,0 +1,391 @@
|
|||
import abc
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
GROUP = "FRR"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
"""
|
||||
Helper to detect MTU mismatch and add the appropriate FRR
|
||||
mtu-ignore command. This is needed when e.g. a node is linked via a
|
||||
GreTap device.
|
||||
"""
|
||||
if ifc.mtu != 1500:
|
||||
return True
|
||||
if not ifc.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
def get_router_id(node: CoreNodeBase) -> str:
|
||||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
class FRRZebra(ConfigService):
|
||||
name = "FRRzebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"]
|
||||
files = [
|
||||
"/usr/local/etc/frr/frr.conf",
|
||||
"frrboot.sh",
|
||||
"/usr/local/etc/frr/vtysh.conf",
|
||||
"/usr/local/etc/frr/daemons",
|
||||
]
|
||||
executables = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh frrboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
frr_conf = self.files[0]
|
||||
frr_bin_search = self.node.session.options.get_config(
|
||||
"frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr"
|
||||
).strip('"')
|
||||
frr_sbin_search = self.node.session.options.get_config(
|
||||
"frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr"
|
||||
).strip('"')
|
||||
|
||||
services = []
|
||||
want_ip4 = False
|
||||
want_ip6 = False
|
||||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
frr_conf=frr_conf,
|
||||
frr_sbin_search=frr_sbin_search,
|
||||
frr_bin_search=frr_bin_search,
|
||||
frr_state_dir=constants.FRR_STATE_DIR,
|
||||
interfaces=interfaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
)
|
||||
|
||||
|
||||
class FrrService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["FRRzebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def frr_config(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FRROspfv2(FrrService, ConfigService):
|
||||
"""
|
||||
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 = "FRROSPFv2"
|
||||
startup = ()
|
||||
shutdown = ["killall ospfd"]
|
||||
validate = ["pidof ospfd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
router-id ${router_id}
|
||||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class FRROspfv3(FrrService, ConfigService):
|
||||
"""
|
||||
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 = "FRROSPFv3"
|
||||
shutdown = ["killall ospf6d"]
|
||||
validate = ["pidof ospf6d"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
router-id ${router_id}
|
||||
% for ifname in ifnames:
|
||||
interface ${ifname} area 0.0.0.0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRBgp(FrrService, ConfigService):
|
||||
"""
|
||||
The BGP service provides interdomain routing.
|
||||
Peers must be manually configured, with a full mesh for those
|
||||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "FRRBGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
custom_needed = True
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
! You should configure the AS number below
|
||||
! along with this router's peers.
|
||||
router bgp {self.node.id}
|
||||
bgp router-id {router_id}
|
||||
redistribute connected
|
||||
!neighbor 1.2.3.4 remote-as 555
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRRip(FrrService, ConfigService):
|
||||
"""
|
||||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
router rip
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf
|
||||
network 0.0.0.0/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRRipng(FrrService, ConfigService):
|
||||
"""
|
||||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "FRRRIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
text = """
|
||||
router ripng
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf6
|
||||
network ::/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class FRRBabel(FrrService, ConfigService):
|
||||
"""
|
||||
The Babel service provides a loop-avoiding distance-vector routing
|
||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "FRRBabel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
network ${ifname}
|
||||
% endfor
|
||||
redistribute static
|
||||
redistribute ipv4 connected
|
||||
!
|
||||
"""
|
||||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
"""
|
||||
else:
|
||||
text = """
|
||||
babel wired
|
||||
babel split-horizon
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class FRRpimd(FrrService, ConfigService):
|
||||
"""
|
||||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "FRRpimd"
|
||||
shutdown = ["killall pimd"]
|
||||
validate = ["pidof pimd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def frr_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
router mfea
|
||||
!
|
||||
router igmp
|
||||
!
|
||||
router pim
|
||||
!ip pim rp-address 10.0.0.1
|
||||
ip pim bsr-candidate {ifname}
|
||||
ip pim rp-candidate {ifname}
|
||||
!ip pim spt-threshold interval 10 bytes 80000
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def frr_interface_config(self, ifc: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip igmp
|
||||
ip pim
|
||||
"""
|
||||
return self.clean_text(text)
|
59
daemon/core/configservices/frrservices/templates/daemons
Normal file
59
daemon/core/configservices/frrservices/templates/daemons
Normal file
|
@ -0,0 +1,59 @@
|
|||
#
|
||||
# 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
|
||||
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.
|
25
daemon/core/configservices/frrservices/templates/frr.conf
Normal file
25
daemon/core/configservices/frrservices/templates/frr.conf
Normal file
|
@ -0,0 +1,25 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if want_ip6:
|
||||
% for addr in ip6s:
|
||||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.frr_interface_config(ifc).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
||||
% for service in services:
|
||||
${service.frr_config()}
|
||||
% endfor
|
95
daemon/core/configservices/frrservices/templates/frrboot.sh
Normal file
95
daemon/core/configservices/frrservices/templates/frrboot.sh
Normal file
|
@ -0,0 +1,95 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by zebra service (frr.py)
|
||||
FRR_CONF="${frr_conf}"
|
||||
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
|
||||
|
||||
#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"
|
||||
for r in rip ripng ospf6 ospf bgp babel; 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
|
|
@ -0,0 +1 @@
|
|||
service integrated-vtysh-config
|
0
daemon/core/configservices/nrlservices/__init__.py
Normal file
0
daemon/core/configservices/nrlservices/__init__.py
Normal file
212
daemon/core/configservices/nrlservices/services.py
Normal file
212
daemon/core/configservices/nrlservices/services.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP = "ProtoSvc"
|
||||
|
||||
|
||||
class MgenSinkService(ConfigService):
|
||||
name = "MGEN_Sink"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["mgensink.sh", "sink.mgen"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh mgensink.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
name = utils.sysctl_devname(ifc.name)
|
||||
ifnames.append(name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlNhdp(ConfigService):
|
||||
name = "NHDP"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlnhdp.sh"]
|
||||
executables = ["nrlnhdp"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlnhdp.sh"]
|
||||
validate = ["pidof nrlnhdp"]
|
||||
shutdown = ["killall nrlnhdp"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class NrlSmf(ConfigService):
|
||||
name = "SMF"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startsmf.sh"]
|
||||
executables = ["nrlsmf", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh startsmf.sh"]
|
||||
validate = ["pidof nrlsmf"]
|
||||
shutdown = ["killall nrlsmf"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_arouted = "arouted" in self.node.config_services
|
||||
has_nhdp = "NHDP" in self.node.config_services
|
||||
has_olsr = "OLSR" in self.node.config_services
|
||||
ifnames = []
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
break
|
||||
return dict(
|
||||
has_arouted=has_arouted,
|
||||
has_nhdp=has_nhdp,
|
||||
has_olsr=has_olsr,
|
||||
ifnames=ifnames,
|
||||
ip4_prefix=ip4_prefix,
|
||||
)
|
||||
|
||||
|
||||
class NrlOlsr(ConfigService):
|
||||
name = "OLSR"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrd.sh"]
|
||||
executables = ["nrlolsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrd.sh"]
|
||||
validate = ["pidof nrlolsrd"]
|
||||
shutdown = ["killall nrlolsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
has_zebra = "zebra" in self.node.config_services
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
break
|
||||
return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
|
||||
|
||||
|
||||
class NrlOlsrv2(ConfigService):
|
||||
name = "OLSRv2"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["nrlolsrv2.sh"]
|
||||
executables = ["nrlolsrv2"]
|
||||
dependencies = []
|
||||
startup = ["sh nrlolsrv2.sh"]
|
||||
validate = ["pidof nrlolsrv2"]
|
||||
shutdown = ["killall nrlolsrv2"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class OlsrOrg(ConfigService):
|
||||
name = "OLSRORG"
|
||||
group = GROUP
|
||||
directories = ["/etc/olsrd"]
|
||||
files = ["olsrd.sh", "/etc/olsrd/olsrd.conf"]
|
||||
executables = ["olsrd"]
|
||||
dependencies = []
|
||||
startup = ["sh olsrd.sh"]
|
||||
validate = ["pidof olsrd"]
|
||||
shutdown = ["killall olsrd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
has_smf = "SMF" in self.node.config_services
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(has_smf=has_smf, ifnames=ifnames)
|
||||
|
||||
|
||||
class MgenActor(ConfigService):
|
||||
name = "MgenActor"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["start_mgen_actor.sh"]
|
||||
executables = ["mgen"]
|
||||
dependencies = []
|
||||
startup = ["sh start_mgen_actor.sh"]
|
||||
validate = ["pidof mgen"]
|
||||
shutdown = ["killall mgen"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Arouted(ConfigService):
|
||||
name = "arouted"
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = ["startarouted.sh"]
|
||||
executables = ["arouted"]
|
||||
dependencies = []
|
||||
startup = ["sh startarouted.sh"]
|
||||
validate = ["pidof arouted"]
|
||||
shutdown = ["pkill arouted"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ip4_prefix = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
if ip4_prefix:
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
ip4_prefix = f"{a}/{24}"
|
||||
break
|
||||
return dict(ip4_prefix=ip4_prefix)
|
|
@ -0,0 +1 @@
|
|||
mgen input sink.mgen output mgen_${node.name}.log
|
|
@ -0,0 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces}
|
|
@ -0,0 +1,9 @@
|
|||
<%
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding s-mpr -smfClient %s_smf" % node.name
|
||||
zebra = ""
|
||||
if has_zebra:
|
||||
zebra = "-z"
|
||||
%>
|
||||
nrlolsrd -i ${ifname} -l /var/log/nrlolsrd.log -rpipe ${node.name}_olsr ${smf} ${zebra}
|
|
@ -0,0 +1,7 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
smf = ""
|
||||
if has_smf:
|
||||
smf = "-flooding ecds -smfClient %s_smf" % node.name
|
||||
%>
|
||||
nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces}
|
312
daemon/core/configservices/nrlservices/templates/olsrd.conf
Normal file
312
daemon/core/configservices/nrlservices/templates/olsrd.conf
Normal file
|
@ -0,0 +1,312 @@
|
|||
#
|
||||
# OLSR.org routing daemon config file
|
||||
# This file contains the usual options for an ETX based
|
||||
# stationary network without fisheye
|
||||
# (for other options see olsrd.conf.default.full)
|
||||
#
|
||||
# Lines starting with a # are discarded
|
||||
#
|
||||
|
||||
#### ATTENTION for IPv6 users ####
|
||||
# Because of limitations in the parser IPv6 addresses must NOT
|
||||
# begin with a ":", so please add a "0" as a prefix.
|
||||
|
||||
###########################
|
||||
### Basic configuration ###
|
||||
###########################
|
||||
# keep this settings at the beginning of your first configuration file
|
||||
|
||||
# Debug level (0-9)
|
||||
# If set to 0 the daemon runs in the background, unless "NoFork" is set to true
|
||||
# (Default is 1)
|
||||
|
||||
# DebugLevel 1
|
||||
|
||||
# IP version to use (4 or 6)
|
||||
# (Default is 4)
|
||||
|
||||
# IpVersion 4
|
||||
|
||||
#################################
|
||||
### OLSRd agent configuration ###
|
||||
#################################
|
||||
# this parameters control the settings of the routing agent which are not
|
||||
# related to the OLSR protocol and it's extensions
|
||||
|
||||
# FIBMetric controls the metric value of the host-routes OLSRd sets.
|
||||
# - "flat" means that the metric value is always 2. This is the preferred value
|
||||
# because it helps the linux kernel routing to clean up older routes
|
||||
# - "correct" use the hopcount as the metric value.
|
||||
# - "approx" use the hopcount as the metric value too, but does only update the
|
||||
# hopcount if the nexthop changes too
|
||||
# (Default is "flat")
|
||||
|
||||
# FIBMetric "flat"
|
||||
|
||||
#######################################
|
||||
### Linux specific OLSRd extensions ###
|
||||
#######################################
|
||||
# these parameters are only working on linux at the moment
|
||||
|
||||
# SrcIpRoutes tells OLSRd to set the Src flag of host routes to the originator-ip
|
||||
# of the node. In addition to this an additional localhost device is created
|
||||
# to make sure the returning traffic can be received.
|
||||
# (Default is "no")
|
||||
|
||||
# SrcIpRoutes no
|
||||
|
||||
# Specify the proto tag to be used for routes olsr inserts into kernel
|
||||
# currently only implemented for linux
|
||||
# valid values under linux are 1 .. 254
|
||||
# 1 gets remapped by olsrd to 0 UNSPECIFIED (1 is reserved for ICMP redirects)
|
||||
# 2 KERNEL routes (not very wise to use)
|
||||
# 3 BOOT (should in fact not be used by routing daemons)
|
||||
# 4 STATIC
|
||||
# 8 .. 15 various routing daemons (gated, zebra, bird, & co)
|
||||
# (defaults to 0 which gets replaced by an OS-specific default value
|
||||
# under linux 3 (BOOT) (for backward compatibility)
|
||||
|
||||
# RtProto 0
|
||||
|
||||
# Activates (in IPv6 mode) the automatic use of NIIT
|
||||
# (see README-Olsr-Extensions)
|
||||
# (default is "yes")
|
||||
|
||||
# UseNiit yes
|
||||
|
||||
# Activates the smartgateway ipip tunnel feature.
|
||||
# See README-Olsr-Extensions for a description of smartgateways.
|
||||
# (default is "no")
|
||||
|
||||
# SmartGateway no
|
||||
|
||||
# Signals that the server tunnel must always be removed on shutdown,
|
||||
# irrespective of the interface up/down state during startup.
|
||||
# (default is "no")
|
||||
|
||||
# SmartGatewayAlwaysRemoveServerTunnel no
|
||||
|
||||
# Determines the maximum number of gateways that can be in use at any given
|
||||
# time. This setting is used to mitigate the effects of breaking connections
|
||||
# (due to the selection of a new gateway) on a dynamic network.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayUseCount 1
|
||||
|
||||
# Determines the take-down percentage for a non-current smart gateway tunnel.
|
||||
# If the cost of the current smart gateway tunnel is less than this percentage
|
||||
# of the cost of the non-current smart gateway tunnel, then the non-current smart
|
||||
# gateway tunnel is taken down because it is then presumed to be 'too expensive'.
|
||||
# This setting is only relevant when SmartGatewayUseCount is larger than 1;
|
||||
# a value of 0 will result in the tunnels not being taken down proactively.
|
||||
# (default is 0)
|
||||
|
||||
# SmartGatewayTakeDownPercentage 0
|
||||
|
||||
# Determines the policy routing script that is executed during startup and
|
||||
# shutdown of olsrd. The script is only executed when SmartGatewayUseCount
|
||||
# is set to a value larger than 1. The script must setup policy routing
|
||||
# rules such that multi-gateway mode works. A sample script is included.
|
||||
# (default is not set)
|
||||
|
||||
# SmartGatewayPolicyRoutingScript ""
|
||||
|
||||
# Determines the egress interfaces that are part of the multi-gateway setup and
|
||||
# therefore only relevant when SmartGatewayUseCount is larger than 1 (in which
|
||||
# case it must be explicitly set).
|
||||
# (default is not set)
|
||||
|
||||
# SmartGatewayEgressInterfaces ""
|
||||
|
||||
# Determines the routing tables offset for multi-gateway policy routing tables
|
||||
# See the policy routing script for an explanation.
|
||||
# (default is 90)
|
||||
|
||||
# SmartGatewayTablesOffset 90
|
||||
|
||||
# Determines the policy routing rules offset for multi-gateway policy routing
|
||||
# rules. See the policy routing script for an explanation.
|
||||
# (default is 0, which indicates that the rules and tables should be aligned and
|
||||
# puts this value at SmartGatewayTablesOffset - # egress interfaces -
|
||||
# # olsr interfaces)
|
||||
|
||||
# SmartGatewayRulesOffset 87
|
||||
|
||||
# Allows the selection of a smartgateway with NAT (only for IPv4)
|
||||
# (default is "yes")
|
||||
|
||||
# SmartGatewayAllowNAT yes
|
||||
|
||||
# Determines the period (in milliseconds) on which a new smart gateway
|
||||
# selection is performed.
|
||||
# (default is 10000 milliseconds)
|
||||
|
||||
# SmartGatewayPeriod 10000
|
||||
|
||||
# Determines the number of times the link state database must be stable
|
||||
# before a new smart gateway is selected.
|
||||
# (default is 6)
|
||||
|
||||
# SmartGatewayStableCount 6
|
||||
|
||||
# When another gateway than the current one has a cost of less than the cost
|
||||
# of the current gateway multiplied by SmartGatewayThreshold then the smart
|
||||
# gateway is switched to the other gateway. The unit is percentage.
|
||||
# (defaults to 0)
|
||||
|
||||
# SmartGatewayThreshold 0
|
||||
|
||||
# The weighing factor for the gateway uplink bandwidth (exit link, uplink).
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightExitLinkUp 1
|
||||
|
||||
# The weighing factor for the gateway downlink bandwidth (exit link, downlink).
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightExitLinkDown 1
|
||||
|
||||
# The weighing factor for the ETX costs.
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 1)
|
||||
|
||||
# SmartGatewayWeightEtx 1
|
||||
|
||||
# The divider for the ETX costs.
|
||||
# See README-Olsr-Extensions for a description of smart gateways.
|
||||
# (default is 0)
|
||||
|
||||
# SmartGatewayDividerEtx 0
|
||||
|
||||
# Defines what kind of Uplink this node will publish as a
|
||||
# smartgateway. The existence of the uplink is detected by
|
||||
# a route to 0.0.0.0/0, ::ffff:0:0/96 and/or 2000::/3.
|
||||
# possible values are "none", "ipv4", "ipv6", "both"
|
||||
# (default is "both")
|
||||
|
||||
# SmartGatewayUplink "both"
|
||||
|
||||
# Specifies if the local ipv4 uplink use NAT
|
||||
# (default is "yes")
|
||||
|
||||
# SmartGatewayUplinkNAT yes
|
||||
|
||||
# Specifies the speed of the uplink in kilobit/s.
|
||||
# First parameter is upstream, second parameter is downstream
|
||||
# (default is 128/1024)
|
||||
|
||||
# SmartGatewaySpeed 128 1024
|
||||
|
||||
# Specifies the EXTERNAL ipv6 prefix of the uplink. A prefix
|
||||
# length of more than 64 is not allowed.
|
||||
# (default is 0::/0
|
||||
|
||||
# SmartGatewayPrefix 0::/0
|
||||
|
||||
##############################
|
||||
### OLSR protocol settings ###
|
||||
##############################
|
||||
|
||||
# HNA (Host network association) allows the OLSR to announce
|
||||
# additional IPs or IP subnets to the net that are reachable
|
||||
# through this node.
|
||||
# Syntax for HNA4 is "network-address network-mask"
|
||||
# Syntax for HNA6 is "network-address prefix-length"
|
||||
# (default is no HNA)
|
||||
Hna4
|
||||
{
|
||||
# Internet gateway
|
||||
# 0.0.0.0 0.0.0.0
|
||||
# specific small networks reachable through this node
|
||||
# 15.15.0.0 255.255.255.0
|
||||
}
|
||||
Hna6
|
||||
{
|
||||
# Internet gateway
|
||||
# 0:: 0
|
||||
# specific small networks reachable through this node
|
||||
# fec0:2200:106:0:0:0:0:0 48
|
||||
}
|
||||
|
||||
################################
|
||||
### OLSR protocol extensions ###
|
||||
################################
|
||||
|
||||
# Link quality algorithm (only for lq level 2)
|
||||
# (see README-Olsr-Extensions)
|
||||
# - "etx_float", a floating point ETX with exponential aging
|
||||
# - "etx_fpm", same as ext_float, but with integer arithmetic
|
||||
# - "etx_ff" (ETX freifunk), an etx variant which use all OLSR
|
||||
# traffic (instead of only hellos) for ETX calculation
|
||||
# - "etx_ffeth", an incompatible variant of etx_ff that allows
|
||||
# ethernet links with ETX 0.1.
|
||||
# (defaults to "etx_ff")
|
||||
|
||||
# LinkQualityAlgorithm "etx_ff"
|
||||
|
||||
# Fisheye mechanism for TCs (0 meansoff, 1 means on)
|
||||
# (default is 1)
|
||||
|
||||
LinkQualityFishEye 0
|
||||
|
||||
#####################################
|
||||
### Example plugin configurations ###
|
||||
#####################################
|
||||
# Olsrd plugins to load
|
||||
# This must be the absolute path to the file
|
||||
# or the loader will use the following scheme:
|
||||
# - Try the paths in the LD_LIBRARY_PATH
|
||||
# environment variable.
|
||||
# - The list of libraries cached in /etc/ld.so.cache
|
||||
# - /lib, followed by /usr/lib
|
||||
#
|
||||
# the examples in this list are for linux, so check if the plugin is
|
||||
# available if you use windows.
|
||||
# each plugin should have a README file in it's lib subfolder
|
||||
|
||||
# LoadPlugin "olsrd_txtinfo.dll"
|
||||
#LoadPlugin "olsrd_txtinfo.so.0.1"
|
||||
#{
|
||||
# the default port is 2006 but you can change it like this:
|
||||
#PlParam "port" "8080"
|
||||
|
||||
# You can set a "accept" single address to allow to connect to
|
||||
# txtinfo. If no address is specified, then localhost (127.0.0.1)
|
||||
# is allowed by default. txtinfo will only use the first "accept"
|
||||
# parameter specified and will ignore the rest.
|
||||
|
||||
# to allow a specific host:
|
||||
#PlParam "accept" "172.29.44.23"
|
||||
# if you set it to 0.0.0.0, it will accept all connections
|
||||
#PlParam "accept" "0.0.0.0"
|
||||
#}
|
||||
|
||||
#############################################
|
||||
### OLSRD default interface configuration ###
|
||||
#############################################
|
||||
# the default interface section can have the same values as the following
|
||||
# interface configuration. It will allow you so set common options for all
|
||||
# interfaces.
|
||||
|
||||
InterfaceDefaults {
|
||||
Ip4Broadcast 255.255.255.255
|
||||
}
|
||||
|
||||
######################################
|
||||
### OLSRd Interfaces configuration ###
|
||||
######################################
|
||||
# multiple interfaces can be specified for a single configuration block
|
||||
# multiple configuration blocks can be specified
|
||||
|
||||
# WARNING, don't forget to insert your interface names here !
|
||||
#Interface "<OLSRd-Interface1>" "<OLSRd-Interface2>"
|
||||
#{
|
||||
# Interface Mode is used to prevent unnecessary
|
||||
# packet forwarding on switched ethernet interfaces
|
||||
# valid Modes are "mesh" and "ether"
|
||||
# (default is "mesh")
|
||||
|
||||
# Mode "mesh"
|
||||
#}
|
|
@ -0,0 +1,4 @@
|
|||
<%
|
||||
interfaces = "-i " + " -i ".join(ifnames)
|
||||
%>
|
||||
olsrd ${interfaces}
|
|
@ -0,0 +1,4 @@
|
|||
0.0 LISTEN UDP 5000
|
||||
% for ifname in ifnames:
|
||||
0.0 Join 224.225.1.2 INTERFACE ${ifname}
|
||||
% endfor
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by MgenActor service
|
||||
mgenBasicActor.py -n ${node.name} -a 0.0.0.0 < /dev/null > /dev/null 2>&1 &
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
for f in "/tmp/${node.name}_smf"; do
|
||||
count=1
|
||||
until [ -e "$f" ]; do
|
||||
if [ $count -eq 10 ]; then
|
||||
echo "ERROR: nrlmsf pipe not found: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.1
|
||||
count=$(($count + 1))
|
||||
done
|
||||
done
|
||||
|
||||
ip route add ${ip4_prefix} dev lo
|
||||
arouted instance ${node.name}_smf tap ${node.name}_tap stability 10 2>&1 > /var/log/arouted.log &
|
15
daemon/core/configservices/nrlservices/templates/startsmf.sh
Normal file
15
daemon/core/configservices/nrlservices/templates/startsmf.sh
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%
|
||||
interfaces = ",".join(ifnames)
|
||||
arouted = ""
|
||||
if has_arouted:
|
||||
arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
|
||||
if has_nhdp:
|
||||
flood = "ecds"
|
||||
elif has_olsr:
|
||||
flood = "smpr"
|
||||
else:
|
||||
flood = "cf"
|
||||
%>
|
||||
#!/bin/sh
|
||||
# auto-generated by NrlSmf service
|
||||
nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
|
0
daemon/core/configservices/quaggaservices/__init__.py
Normal file
0
daemon/core/configservices/quaggaservices/__init__.py
Normal file
424
daemon/core/configservices/quaggaservices/services.py
Normal file
424
daemon/core/configservices/quaggaservices/services.py
Normal file
|
@ -0,0 +1,424 @@
|
|||
import abc
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import constants
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emane.nodes import EmaneNet
|
||||
from core.nodes.base import CoreNodeBase
|
||||
from core.nodes.interface import CoreInterface
|
||||
from core.nodes.network import WlanNode
|
||||
|
||||
GROUP = "Quagga"
|
||||
|
||||
|
||||
def has_mtu_mismatch(ifc: CoreInterface) -> bool:
|
||||
"""
|
||||
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 ifc.mtu != 1500:
|
||||
return True
|
||||
if not ifc.net:
|
||||
return False
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu != ifc.mtu:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_min_mtu(ifc):
|
||||
"""
|
||||
Helper to discover the minimum MTU of interfaces linked with the
|
||||
given interface.
|
||||
"""
|
||||
mtu = ifc.mtu
|
||||
if not ifc.net:
|
||||
return mtu
|
||||
for i in ifc.net.netifs():
|
||||
if i.mtu < mtu:
|
||||
mtu = i.mtu
|
||||
return mtu
|
||||
|
||||
|
||||
def get_router_id(node: CoreNodeBase) -> str:
|
||||
"""
|
||||
Helper to return the first IPv4 address of a node as its router ID.
|
||||
"""
|
||||
for ifc in node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
a = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(a):
|
||||
return a
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
class Zebra(ConfigService):
|
||||
name = "zebra"
|
||||
group = GROUP
|
||||
directories = ["/usr/local/etc/quagga", "/var/run/quagga"]
|
||||
files = [
|
||||
"/usr/local/etc/quagga/Quagga.conf",
|
||||
"quaggaboot.sh",
|
||||
"/usr/local/etc/quagga/vtysh.conf",
|
||||
]
|
||||
executables = ["zebra"]
|
||||
dependencies = []
|
||||
startup = ["sh quaggaboot.sh zebra"]
|
||||
validate = ["pidof zebra"]
|
||||
shutdown = ["killall zebra"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
quagga_bin_search = self.node.session.options.get_config(
|
||||
"quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_sbin_search = self.node.session.options.get_config(
|
||||
"quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga"
|
||||
).strip('"')
|
||||
quagga_state_dir = constants.QUAGGA_STATE_DIR
|
||||
quagga_conf = self.files[0]
|
||||
|
||||
services = []
|
||||
want_ip4 = False
|
||||
want_ip6 = False
|
||||
for service in self.node.config_services.values():
|
||||
if self.name not in service.dependencies:
|
||||
continue
|
||||
if service.ipv4_routing:
|
||||
want_ip4 = True
|
||||
if service.ipv6_routing:
|
||||
want_ip6 = True
|
||||
services.append(service)
|
||||
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
ip4s = []
|
||||
ip6s = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
ip4s.append(x)
|
||||
else:
|
||||
ip6s.append(x)
|
||||
is_control = getattr(ifc, "control", False)
|
||||
interfaces.append((ifc, ip4s, ip6s, is_control))
|
||||
|
||||
return dict(
|
||||
quagga_bin_search=quagga_bin_search,
|
||||
quagga_sbin_search=quagga_sbin_search,
|
||||
quagga_state_dir=quagga_state_dir,
|
||||
quagga_conf=quagga_conf,
|
||||
interfaces=interfaces,
|
||||
want_ip4=want_ip4,
|
||||
want_ip6=want_ip6,
|
||||
services=services,
|
||||
)
|
||||
|
||||
|
||||
class QuaggaService(abc.ABC):
|
||||
group = GROUP
|
||||
directories = []
|
||||
files = []
|
||||
executables = []
|
||||
dependencies = ["zebra"]
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
ipv4_routing = False
|
||||
ipv6_routing = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def quagga_config(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Ospfv2(QuaggaService, ConfigService):
|
||||
"""
|
||||
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 = "OSPFv2"
|
||||
validate = ["pidof ospfd"]
|
||||
shutdown = ["killall ospfd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if has_mtu_mismatch(ifc):
|
||||
return "ip ospf mtu-ignore"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
addresses = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for a in ifc.addrlist:
|
||||
addr = a.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
addresses.append(a)
|
||||
data = dict(router_id=router_id, addresses=addresses)
|
||||
text = """
|
||||
router ospf
|
||||
router-id ${router_id}
|
||||
% for addr in addresses:
|
||||
network ${addr} area 0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
|
||||
class Ospfv3(QuaggaService, ConfigService):
|
||||
"""
|
||||
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 = "OSPFv3"
|
||||
shutdown = ("killall ospf6d",)
|
||||
validate = ("pidof ospf6d",)
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
mtu = get_min_mtu(ifc)
|
||||
if mtu < ifc.mtu:
|
||||
return f"ipv6 ospf6 ifmtu {mtu}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
data = dict(router_id=router_id, ifnames=ifnames)
|
||||
text = """
|
||||
router ospf6
|
||||
instance-id 65
|
||||
router-id ${router_id}
|
||||
% for ifname in ifnames:
|
||||
interface ${ifname} area 0.0.0.0
|
||||
% endfor
|
||||
!
|
||||
"""
|
||||
return self.render_text(text, data)
|
||||
|
||||
|
||||
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 = "OSPFv3MDR"
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
for ifc in self.node.netifs():
|
||||
is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet))
|
||||
logging.info("MDR wireless: %s", is_wireless)
|
||||
return dict()
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
config = super().quagga_interface_config(ifc)
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
config = self.clean_text(
|
||||
f"""
|
||||
{config}
|
||||
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
|
||||
"""
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
class Bgp(QuaggaService, ConfigService):
|
||||
"""
|
||||
The BGP service provides interdomain routing.
|
||||
Peers must be manually configured, with a full mesh for those
|
||||
having the same AS number.
|
||||
"""
|
||||
|
||||
name = "BGP"
|
||||
shutdown = ["killall bgpd"]
|
||||
validate = ["pidof bgpd"]
|
||||
ipv4_routing = True
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
return ""
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
router_id = get_router_id(self.node)
|
||||
text = f"""
|
||||
! BGP configuration
|
||||
! You should configure the AS number below
|
||||
! along with this router's peers.
|
||||
router bgp {self.node.id}
|
||||
bgp router-id {router_id}
|
||||
redistribute connected
|
||||
!neighbor 1.2.3.4 remote-as 555
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class Rip(QuaggaService, ConfigService):
|
||||
"""
|
||||
The RIP service provides IPv4 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIP"
|
||||
shutdown = ["killall ripd"]
|
||||
validate = ["pidof ripd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
router rip
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf
|
||||
network 0.0.0.0/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Ripng(QuaggaService, ConfigService):
|
||||
"""
|
||||
The RIP NG service provides IPv6 routing for wired networks.
|
||||
"""
|
||||
|
||||
name = "RIPNG"
|
||||
shutdown = ["killall ripngd"]
|
||||
validate = ["pidof ripngd"]
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
text = """
|
||||
router ripng
|
||||
redistribute static
|
||||
redistribute connected
|
||||
redistribute ospf6
|
||||
network ::/0
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
class Babel(QuaggaService, ConfigService):
|
||||
"""
|
||||
The Babel service provides a loop-avoiding distance-vector routing
|
||||
protocol for IPv6 and IPv4 with fast convergence properties.
|
||||
"""
|
||||
|
||||
name = "Babel"
|
||||
shutdown = ["killall babeld"]
|
||||
validate = ["pidof babeld"]
|
||||
ipv6_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
text = """
|
||||
router babel
|
||||
% for ifname in ifnames:
|
||||
network ${ifname}
|
||||
% endfor
|
||||
redistribute static
|
||||
redistribute connected
|
||||
!
|
||||
"""
|
||||
data = dict(ifnames=ifnames)
|
||||
return self.render_text(text, data)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
if isinstance(ifc.net, (WlanNode, EmaneNet)):
|
||||
text = """
|
||||
babel wireless
|
||||
no babel split-horizon
|
||||
"""
|
||||
else:
|
||||
text = """
|
||||
babel wired
|
||||
babel split-horizon
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
|
||||
class Xpimd(QuaggaService, ConfigService):
|
||||
"""
|
||||
PIM multicast routing based on XORP.
|
||||
"""
|
||||
|
||||
name = "Xpimd"
|
||||
shutdown = ["killall xpimd"]
|
||||
validate = ["pidof xpimd"]
|
||||
ipv4_routing = True
|
||||
|
||||
def quagga_config(self) -> str:
|
||||
ifname = "eth0"
|
||||
for ifc in self.node.netifs():
|
||||
if ifc.name != "lo":
|
||||
ifname = ifc.name
|
||||
break
|
||||
|
||||
text = f"""
|
||||
router mfea
|
||||
!
|
||||
router igmp
|
||||
!
|
||||
router pim
|
||||
!ip pim rp-address 10.0.0.1
|
||||
ip pim bsr-candidate {ifname}
|
||||
ip pim rp-candidate {ifname}
|
||||
!ip pim spt-threshold interval 10 bytes 80000
|
||||
!
|
||||
"""
|
||||
return self.clean_text(text)
|
||||
|
||||
def quagga_interface_config(self, ifc: CoreInterface) -> str:
|
||||
text = """
|
||||
ip mfea
|
||||
ip pim
|
||||
"""
|
||||
return self.clean_text(text)
|
|
@ -0,0 +1,25 @@
|
|||
% for ifc, ip4s, ip6s, is_control in interfaces:
|
||||
interface ${ifc.name}
|
||||
% if want_ip4:
|
||||
% for addr in ip4s:
|
||||
ip address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if want_ip6:
|
||||
% for addr in ip6s:
|
||||
ipv6 address ${addr}
|
||||
% endfor
|
||||
% endif
|
||||
% if not is_control:
|
||||
% for service in services:
|
||||
% for line in service.quagga_interface_config(ifc).split("\n"):
|
||||
${line}
|
||||
% endfor
|
||||
% endfor
|
||||
% endif
|
||||
!
|
||||
% endfor
|
||||
|
||||
% for service in services:
|
||||
${service.quagga_config()}
|
||||
% endfor
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by zebra service (quagga.py)
|
||||
QUAGGA_CONF="${quagga_conf}"
|
||||
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
|
|
@ -0,0 +1 @@
|
|||
service integrated-vtysh-config
|
0
daemon/core/configservices/sercurityservices/__init__.py
Normal file
0
daemon/core/configservices/sercurityservices/__init__.py
Normal file
88
daemon/core/configservices/sercurityservices/services.py
Normal file
88
daemon/core/configservices/sercurityservices/services.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP_NAME = "Security"
|
||||
|
||||
|
||||
class VpnClient(ConfigService):
|
||||
name = "VPNClient"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["vpnclient.sh"]
|
||||
executables = ["openvpn", "ip", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh vpnclient.sh"]
|
||||
validate = ["pidof openvpn"]
|
||||
shutdown = ["killall openvpn"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class VPNServer(ConfigService):
|
||||
name = "VPNServer"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["vpnserver.sh"]
|
||||
executables = ["openvpn", "ip", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh vpnserver.sh"]
|
||||
validate = ["pidof openvpn"]
|
||||
shutdown = ["killall openvpn"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class IPsec(ConfigService):
|
||||
name = "IPsec"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["ipsec.sh"]
|
||||
executables = ["racoon", "ip", "setkey", "killall"]
|
||||
dependencies = []
|
||||
startup = ["sh ipsec.sh"]
|
||||
validate = ["pidof racoon"]
|
||||
shutdown = ["killall racoon"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Firewall(ConfigService):
|
||||
name = "Firewall"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["firewall.sh"]
|
||||
executables = ["iptables"]
|
||||
dependencies = []
|
||||
startup = ["sh firewall.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class Nat(ConfigService):
|
||||
name = "NAT"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["nat.sh"]
|
||||
executables = ["iptables"]
|
||||
dependencies = []
|
||||
startup = ["sh nat.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(ifnames=ifnames)
|
|
@ -0,0 +1,30 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# Below are sample iptables firewall rules that you can uncomment and edit.
|
||||
# You can also use ip6tables rules for IPv6.
|
||||
#
|
||||
|
||||
# start by flushing all firewall rules (so this script may be re-run)
|
||||
#iptables -F
|
||||
|
||||
# allow traffic related to established connections
|
||||
#iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# allow TCP packets from any source destined for 192.168.1.1
|
||||
#iptables -A INPUT -s 0/0 -i eth0 -d 192.168.1.1 -p TCP -j ACCEPT
|
||||
|
||||
# allow OpenVPN server traffic from eth0
|
||||
#iptables -A INPUT -p udp --dport 1194 -j ACCEPT
|
||||
#iptables -A INPUT -i eth0 -j DROP
|
||||
#iptables -A OUTPUT -p udp --sport 1194 -j ACCEPT
|
||||
#iptables -A OUTPUT -o eth0 -j DROP
|
||||
|
||||
# allow ICMP ping traffic
|
||||
#iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
#iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
|
||||
# allow SSH traffic
|
||||
#iptables -A -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
|
||||
|
||||
# drop all other traffic coming in eth0
|
||||
#iptables -A INPUT -i eth0 -j DROP
|
114
daemon/core/configservices/sercurityservices/templates/ipsec.sh
Normal file
114
daemon/core/configservices/sercurityservices/templates/ipsec.sh
Normal file
|
@ -0,0 +1,114 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The IPsec service builds ESP tunnels between the specified peers using the
|
||||
# racoon IKEv2 keying daemon. You need to provide keys and the addresses of
|
||||
# peers, along with subnets to tunnel.
|
||||
|
||||
# directory containing the certificate and key described below
|
||||
keydir=/etc/core/keys
|
||||
|
||||
# the name used for the "$certname.pem" x509 certificate and
|
||||
# "$certname.key" RSA private key, which can be generated using openssl
|
||||
certname=ipsec1
|
||||
|
||||
# list the public-facing IP addresses, starting with the localhost and followed
|
||||
# by each tunnel peer, separated with a single space
|
||||
tunnelhosts="172.16.0.1AND172.16.0.2 172.16.0.1AND172.16.2.1"
|
||||
|
||||
# Define T<i> where i is the index for each tunnel peer host from
|
||||
# the tunnel_hosts list above (0 is localhost).
|
||||
# T<i> is a list of IPsec tunnels with peer i, with a local subnet address
|
||||
# followed by the remote subnet address:
|
||||
# T<i>="<local>AND<remote> <local>AND<remote>"
|
||||
# For example, 172.16.0.0/24 is a local network (behind this node) to be
|
||||
# tunneled and 172.16.2.0/24 is a remote network (behind peer 1)
|
||||
T1="172.16.3.0/24AND172.16.5.0/24"
|
||||
T2="172.16.4.0/24AND172.16.5.0/24 172.16.4.0/24AND172.16.6.0/24"
|
||||
|
||||
# -------- END CUSTOMIZATION --------
|
||||
|
||||
echo "building config $PWD/ipsec.conf..."
|
||||
echo "building config $PWD/ipsec.conf..." > $PWD/ipsec.log
|
||||
|
||||
checkip=0
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed
|
||||
" >> $PWD/ipsec.log
|
||||
checkip=1
|
||||
fi
|
||||
|
||||
echo "#!/usr/sbin/setkey -f
|
||||
# Flush the SAD and SPD
|
||||
flush;
|
||||
spdflush;
|
||||
|
||||
# Security policies " > $PWD/ipsec.conf
|
||||
i=0
|
||||
for hostpair in $tunnelhosts; do
|
||||
i=`expr $i + 1`
|
||||
# parse tunnel host IP
|
||||
thishost=$${}{hostpair%%AND*}
|
||||
peerhost=$${}{hostpair##*AND}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$thishost" "$peerhost" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid host address $thishost or $peerhost " >> $PWD/ipsec.log
|
||||
fi
|
||||
# parse each tunnel addresses
|
||||
tunnel_list_var_name=T$i
|
||||
eval tunnels="$"$tunnel_list_var_name""
|
||||
for ttunnel in $tunnels; do
|
||||
lclnet=$${}{ttunnel%%AND*}
|
||||
rmtnet=$${}{ttunnel##*AND}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$lclnet" "$rmtnet"| grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid tunnel address $lclnet and $rmtnet " >> $PWD/ipsec.log
|
||||
fi
|
||||
# add tunnel policies
|
||||
echo "
|
||||
spdadd $lclnet $rmtnet any -P out ipsec
|
||||
esp/tunnel/$thishost-$peerhost/require;
|
||||
spdadd $rmtnet $lclnet any -P in ipsec
|
||||
esp/tunnel/$peerhost-$thishost/require; " >> $PWD/ipsec.conf
|
||||
done
|
||||
done
|
||||
|
||||
echo "building config $PWD/racoon.conf..."
|
||||
if [ ! -e $keydir\/$certname.key ] || [ ! -e $keydir\/$certname.pem ]; then
|
||||
echo "ERROR: missing certification files under $keydir $certname.key or $certname.pem " >> $PWD/ipsec.log
|
||||
fi
|
||||
echo "
|
||||
path certificate \"$keydir\";
|
||||
listen {
|
||||
adminsock disabled;
|
||||
}
|
||||
remote anonymous
|
||||
{
|
||||
exchange_mode main;
|
||||
certificate_type x509 \"$certname.pem\" \"$certname.key\";
|
||||
ca_type x509 \"ca-cert.pem\";
|
||||
my_identifier asn1dn;
|
||||
peers_identifier asn1dn;
|
||||
|
||||
proposal {
|
||||
encryption_algorithm 3des ;
|
||||
hash_algorithm sha1;
|
||||
authentication_method rsasig ;
|
||||
dh_group modp768;
|
||||
}
|
||||
}
|
||||
sainfo anonymous
|
||||
{
|
||||
pfs_group modp768;
|
||||
lifetime time 1 hour ;
|
||||
encryption_algorithm 3des, blowfish 448, rijndael ;
|
||||
authentication_algorithm hmac_sha1, hmac_md5 ;
|
||||
compression_algorithm deflate ;
|
||||
}
|
||||
" > $PWD/racoon.conf
|
||||
|
||||
# the setkey program is required from the ipsec-tools package
|
||||
echo "running setkey -f $PWD/ipsec.conf..."
|
||||
setkey -f $PWD/ipsec.conf
|
||||
|
||||
echo "running racoon -d -f $PWD/racoon.conf..."
|
||||
racoon -d -f $PWD/racoon.conf -l racoon.log
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
# generated by security.py
|
||||
# NAT out the first interface by default
|
||||
% for index, ifname in enumerate(ifnames):
|
||||
% if index == 0:
|
||||
iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE
|
||||
iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -A FORWARD -i ${ifname} -j DROP
|
||||
% else:
|
||||
# iptables -t nat -A POSTROUTING -o ${ifname} -j MASQUERADE
|
||||
# iptables -A FORWARD -i ${ifname} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# iptables -A FORWARD -i ${ifname} -j DROP
|
||||
% endif
|
||||
% endfor
|
|
@ -0,0 +1,61 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The VPNClient service builds a VPN tunnel to the specified VPN server using
|
||||
# OpenVPN software and a virtual TUN/TAP device.
|
||||
|
||||
# directory containing the certificate and key described below
|
||||
keydir=/etc/core/keys
|
||||
|
||||
# the name used for a "$keyname.crt" certificate and "$keyname.key" private key.
|
||||
keyname=client1
|
||||
|
||||
# the public IP address of the VPN server this client should connect with
|
||||
vpnserver="10.0.2.10"
|
||||
|
||||
# optional next hop for adding a static route to reach the VPN server
|
||||
nexthop="10.0.1.1"
|
||||
|
||||
# --------- END CUSTOMIZATION --------
|
||||
|
||||
# validate addresses
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed
|
||||
" > $PWD/vpnclient.log
|
||||
else
|
||||
if [ "$(sipcalc "$vpnserver" "$nexthop" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalide address $vpnserver or $nexthop " > $PWD/vpnclient.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# validate key and certification files
|
||||
if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \
|
||||
|| [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then
|
||||
echo "ERROR: missing certification or key files under $keydir $keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnclient.log
|
||||
fi
|
||||
|
||||
# if necessary, add a static route for reaching the VPN server IP via the IF
|
||||
vpnservernet=$${}{vpnserver%.*}.0/24
|
||||
if [ "$nexthop" != "" ]; then
|
||||
ip route add $vpnservernet via $nexthop
|
||||
fi
|
||||
|
||||
# create openvpn client.conf
|
||||
(
|
||||
cat << EOF
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
remote $vpnserver 1194
|
||||
nobind
|
||||
ca $keydir/ca.crt
|
||||
cert $keydir/$keyname.crt
|
||||
key $keydir/$keyname.key
|
||||
dh $keydir/dh1024.pem
|
||||
cipher AES-256-CBC
|
||||
log $PWD/openvpn-client.log
|
||||
verb 4
|
||||
daemon
|
||||
EOF
|
||||
) > client.conf
|
||||
|
||||
openvpn --config client.conf
|
|
@ -0,0 +1,147 @@
|
|||
# -------- CUSTOMIZATION REQUIRED --------
|
||||
#
|
||||
# The VPNServer service sets up the OpenVPN server for building VPN tunnels
|
||||
# that allow access via TUN/TAP device to private networks.
|
||||
#
|
||||
# note that the IPForward and DefaultRoute services should be enabled
|
||||
|
||||
# directory containing the certificate and key described below, in addition to
|
||||
# a CA certificate and DH key
|
||||
keydir=/etc/core/keys
|
||||
|
||||
# the name used for a "$keyname.crt" certificate and "$keyname.key" private key.
|
||||
keyname=server2
|
||||
|
||||
# the VPN subnet address from which the client VPN IP (for the TUN/TAP)
|
||||
# will be allocated
|
||||
vpnsubnet=10.0.200.0
|
||||
|
||||
# public IP address of this vpn server (same as VPNClient vpnserver= setting)
|
||||
vpnserver=10.0.2.10
|
||||
|
||||
# optional list of private subnets reachable behind this VPN server
|
||||
# each subnet and next hop is separated by a space
|
||||
# "<subnet1>,<nexthop1> <subnet2>,<nexthop2> ..."
|
||||
privatenets="10.0.11.0,10.0.10.1 10.0.12.0,10.0.10.1"
|
||||
|
||||
# optional list of VPN clients, for statically assigning IP addresses to
|
||||
# clients; also, an optional client subnet can be specified for adding static
|
||||
# routes via the client
|
||||
# Note: VPN addresses x.x.x.0-3 are reserved
|
||||
# "<keyname>,<vpnIP>,<subnetIP> <keyname>,<vpnIP>,<subnetIP> ..."
|
||||
vpnclients="client1KeyFilename,10.0.200.5,10.0.0.0 client2KeyFilename,,"
|
||||
|
||||
# NOTE: you may need to enable the StaticRoutes service on nodes within the
|
||||
# private subnet, in order to have routes back to the client.
|
||||
# /sbin/ip ro add <vpnsubnet>/24 via <vpnServerRemoteInterface>
|
||||
# /sbin/ip ro add <vpnClientSubnet>/24 via <vpnServerRemoteInterface>
|
||||
|
||||
# -------- END CUSTOMIZATION --------
|
||||
|
||||
echo > $PWD/vpnserver.log
|
||||
rm -f -r $PWD/ccd
|
||||
|
||||
# validate key and certification files
|
||||
if [ ! -e $keydir\/$keyname.key ] || [ ! -e $keydir\/$keyname.crt ] \
|
||||
|| [ ! -e $keydir\/ca.crt ] || [ ! -e $keydir\/dh1024.pem ]; then
|
||||
echo "ERROR: missing certification or key files under $keydir \
|
||||
$keyname.key or $keyname.crt or ca.crt or dh1024.pem" >> $PWD/vpnserver.log
|
||||
fi
|
||||
|
||||
# validate configuration IP addresses
|
||||
checkip=0
|
||||
if [ "$(dpkg -l | grep " sipcalc ")" = "" ]; then
|
||||
echo "WARNING: ip validation disabled because package sipcalc not installed\
|
||||
" >> $PWD/vpnserver.log
|
||||
checkip=1
|
||||
else
|
||||
if [ "$(sipcalc "$vpnsubnet" "$vpnserver" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn subnet or server address \
|
||||
$vpnsubnet or $vpnserver " >> $PWD/vpnserver.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# create client vpn ip pool file
|
||||
(
|
||||
cat << EOF
|
||||
EOF
|
||||
)> $PWD/ippool.txt
|
||||
|
||||
# create server.conf file
|
||||
(
|
||||
cat << EOF
|
||||
# openvpn server config
|
||||
local $vpnserver
|
||||
server $vpnsubnet 255.255.255.0
|
||||
push redirect-gateway def1
|
||||
EOF
|
||||
)> $PWD/server.conf
|
||||
|
||||
# add routes to VPN server private subnets, and push these routes to clients
|
||||
for privatenet in $privatenets; do
|
||||
if [ $privatenet != "" ]; then
|
||||
net=$${}{privatenet%%,*}
|
||||
nexthop=$${}{privatenet##*,}
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$net" "$nexthop" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn server private net address \
|
||||
$net or $nexthop " >> $PWD/vpnserver.log
|
||||
fi
|
||||
echo push route $net 255.255.255.0 >> $PWD/server.conf
|
||||
ip ro add $net/24 via $nexthop
|
||||
ip ro add $vpnsubnet/24 via $nexthop
|
||||
fi
|
||||
done
|
||||
|
||||
# allow subnet through this VPN, one route for each client subnet
|
||||
for client in $vpnclients; do
|
||||
if [ $client != "" ]; then
|
||||
cSubnetIP=$${}{client##*,}
|
||||
cVpnIP=$${}{client#*,}
|
||||
cVpnIP=$${}{cVpnIP%%,*}
|
||||
cKeyFilename=$${}{client%%,*}
|
||||
if [ "$cSubnetIP" != "" ]; then
|
||||
if [ $checkip = "0" ] &&
|
||||
[ "$(sipcalc "$cSubnetIP" "$cVpnIP" | grep ERR)" != "" ]; then
|
||||
echo "ERROR: invalid vpn client and subnet address \
|
||||
$cSubnetIP or $cVpnIP " >> $PWD/vpnserver.log
|
||||
fi
|
||||
echo route $cSubnetIP 255.255.255.0 >> $PWD/server.conf
|
||||
if ! test -d $PWD/ccd; then
|
||||
mkdir -p $PWD/ccd
|
||||
echo client-config-dir $PWD/ccd >> $PWD/server.conf
|
||||
fi
|
||||
if test -e $PWD/ccd/$cKeyFilename; then
|
||||
echo iroute $cSubnetIP 255.255.255.0 >> $PWD/ccd/$cKeyFilename
|
||||
else
|
||||
echo iroute $cSubnetIP 255.255.255.0 > $PWD/ccd/$cKeyFilename
|
||||
fi
|
||||
fi
|
||||
if [ "$cVpnIP" != "" ]; then
|
||||
echo $cKeyFilename,$cVpnIP >> $PWD/ippool.txt
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
(
|
||||
cat << EOF
|
||||
keepalive 10 120
|
||||
ca $keydir/ca.crt
|
||||
cert $keydir/$keyname.crt
|
||||
key $keydir/$keyname.key
|
||||
dh $keydir/dh1024.pem
|
||||
cipher AES-256-CBC
|
||||
status /var/log/openvpn-status.log
|
||||
log /var/log/openvpn-server.log
|
||||
ifconfig-pool-linear
|
||||
ifconfig-pool-persist $PWD/ippool.txt
|
||||
port 1194
|
||||
proto udp
|
||||
dev tun
|
||||
verb 4
|
||||
daemon
|
||||
EOF
|
||||
)>> $PWD/server.conf
|
||||
|
||||
# start vpn server
|
||||
openvpn --config server.conf
|
47
daemon/core/configservices/simpleservice.py
Normal file
47
daemon/core/configservices/simpleservice.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
from core.config import Configuration
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
from core.emulator.enumerations import ConfigDataTypes
|
||||
|
||||
|
||||
class SimpleService(ConfigService):
|
||||
name = "Simple"
|
||||
group = "SimpleGroup"
|
||||
directories = ["/etc/quagga", "/usr/local/lib"]
|
||||
files = ["test1.sh", "test2.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = []
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = [
|
||||
Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"),
|
||||
Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"),
|
||||
Configuration(
|
||||
_id="value3",
|
||||
_type=ConfigDataTypes.STRING,
|
||||
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:
|
||||
if name == "test1.sh":
|
||||
return """
|
||||
# sample script 1
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello
|
||||
"""
|
||||
elif name == "test2.sh":
|
||||
return """
|
||||
# sample script 2
|
||||
# node id(${node.id}) name(${node.name})
|
||||
# config: ${config}
|
||||
echo hello2
|
||||
"""
|
0
daemon/core/configservices/utilservices/__init__.py
Normal file
0
daemon/core/configservices/utilservices/__init__.py
Normal file
302
daemon/core/configservices/utilservices/services.py
Normal file
302
daemon/core/configservices/utilservices/services.py
Normal file
|
@ -0,0 +1,302 @@
|
|||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.configservice.base import ConfigService, ConfigServiceMode
|
||||
|
||||
GROUP_NAME = "Utility"
|
||||
|
||||
|
||||
class DefaultRouteService(ConfigService):
|
||||
name = "DefaultRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultroute.sh"]
|
||||
executables = ["ip"]
|
||||
dependencies = []
|
||||
startup = ["sh defaultroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
addresses = []
|
||||
for netif in self.node.netifs():
|
||||
if getattr(netif, "control", False):
|
||||
continue
|
||||
for addr in netif.addrlist:
|
||||
logging.info("default route address: %s", addr)
|
||||
net = netaddr.IPNetwork(addr)
|
||||
if net[1] != net[-2]:
|
||||
addresses.append(net[1])
|
||||
return dict(addresses=addresses)
|
||||
|
||||
|
||||
class DefaultMulticastRouteService(ConfigService):
|
||||
name = "DefaultMulticastRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["defaultmroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh defaultmroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifname = None
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifname = ifc.name
|
||||
break
|
||||
return dict(ifname=ifname)
|
||||
|
||||
|
||||
class StaticRouteService(ConfigService):
|
||||
name = "StaticRoute"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["staticroute.sh"]
|
||||
executables = []
|
||||
dependencies = []
|
||||
startup = ["sh staticroute.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
routes = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
dst = "3ffe:4::/64"
|
||||
else:
|
||||
dst = "10.9.8.0/24"
|
||||
net = netaddr.IPNetwork(x)
|
||||
if net[-2] != net[1]:
|
||||
routes.append((dst, net[1]))
|
||||
return dict(routes=routes)
|
||||
|
||||
|
||||
class IpForwardService(ConfigService):
|
||||
name = "IPForward"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["ipforward.sh"]
|
||||
executables = ["sysctl"]
|
||||
dependencies = []
|
||||
startup = ["sh ipforward.sh"]
|
||||
validate = []
|
||||
shutdown = []
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
devnames = []
|
||||
for ifc in self.node.netifs():
|
||||
devname = utils.sysctl_devname(ifc.name)
|
||||
devnames.append(devname)
|
||||
return dict(devnames=devnames)
|
||||
|
||||
|
||||
class SshService(ConfigService):
|
||||
name = "SSH"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/ssh", "/var/run/sshd"]
|
||||
files = ["startsshd.sh", "/etc/ssh/sshd_config"]
|
||||
executables = ["sshd"]
|
||||
dependencies = []
|
||||
startup = ["sh startsshd.sh"]
|
||||
validate = []
|
||||
shutdown = ["killall sshd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
return dict(
|
||||
sshcfgdir=self.directories[0],
|
||||
sshstatedir=self.directories[1],
|
||||
sshlibdir="/usr/lib/openssh",
|
||||
)
|
||||
|
||||
|
||||
class DhcpService(ConfigService):
|
||||
name = "DHCP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/dhcp", "/var/lib/dhcp"]
|
||||
files = ["/etc/dhcp/dhcpd.conf"]
|
||||
executables = ["dhcpd"]
|
||||
dependencies = []
|
||||
startup = ["touch /var/lib/dhcp/dhcpd.leases", "dhcpd"]
|
||||
validate = ["pidof dhcpd"]
|
||||
shutdown = ["killall dhcpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
subnets = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv4(addr):
|
||||
net = netaddr.IPNetwork(x)
|
||||
# divide the address space in half
|
||||
index = (net.size - 2) / 2
|
||||
rangelow = net[index]
|
||||
rangehigh = net[-2]
|
||||
subnets.append((net.ip, net.netmask, rangelow, rangehigh, addr))
|
||||
return dict(subnets=subnets)
|
||||
|
||||
|
||||
class DhcpClientService(ConfigService):
|
||||
name = "DHCPClient"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["startdhcpclient.sh"]
|
||||
executables = ["dhclient"]
|
||||
dependencies = []
|
||||
startup = ["sh startdhcpclient.sh"]
|
||||
validate = ["pidof dhclient"]
|
||||
shutdown = ["killall dhclient"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict(ifnames=ifnames)
|
||||
|
||||
|
||||
class FtpService(ConfigService):
|
||||
name = "FTP"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/run/vsftpd/empty", "/var/ftp"]
|
||||
files = ["vsftpd.conf"]
|
||||
executables = ["vsftpd"]
|
||||
dependencies = []
|
||||
startup = ["vsftpd ./vsftpd.conf"]
|
||||
validate = ["pidof vsftpd"]
|
||||
shutdown = ["killall vsftpd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class PcapService(ConfigService):
|
||||
name = "pcap"
|
||||
group = GROUP_NAME
|
||||
directories = []
|
||||
files = ["pcap.sh"]
|
||||
executables = ["tcpdump"]
|
||||
dependencies = []
|
||||
startup = ["sh pcap.sh start"]
|
||||
validate = ["pidof tcpdump"]
|
||||
shutdown = ["sh pcap.sh stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
ifnames = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
ifnames.append(ifc.name)
|
||||
return dict()
|
||||
|
||||
|
||||
class RadvdService(ConfigService):
|
||||
name = "radvd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/etc/radvd"]
|
||||
files = ["/etc/radvd/radvd.conf"]
|
||||
executables = ["radvd"]
|
||||
dependencies = []
|
||||
startup = ["radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log"]
|
||||
validate = ["pidof radvd"]
|
||||
shutdown = ["pkill radvd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
prefixes = []
|
||||
for x in ifc.addrlist:
|
||||
addr = x.split("/")[0]
|
||||
if netaddr.valid_ipv6(addr):
|
||||
prefixes.append(x)
|
||||
if not prefixes:
|
||||
continue
|
||||
interfaces.append((ifc.name, prefixes))
|
||||
return dict(interfaces=interfaces)
|
||||
|
||||
|
||||
class AtdService(ConfigService):
|
||||
name = "atd"
|
||||
group = GROUP_NAME
|
||||
directories = ["/var/spool/cron/atjobs", "/var/spool/cron/atspool"]
|
||||
files = ["startatd.sh"]
|
||||
executables = ["atd"]
|
||||
dependencies = []
|
||||
startup = ["sh startatd.sh"]
|
||||
validate = ["pidof atd"]
|
||||
shutdown = ["pkill atd"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
|
||||
class HttpService(ConfigService):
|
||||
name = "HTTP"
|
||||
group = GROUP_NAME
|
||||
directories = [
|
||||
"/etc/apache2",
|
||||
"/var/run/apache2",
|
||||
"/var/log/apache2",
|
||||
"/run/lock",
|
||||
"/var/lock/apache2",
|
||||
"/var/www",
|
||||
]
|
||||
files = ["/etc/apache2/apache2.conf", "/etc/apache2/envvars", "/var/www/index.html"]
|
||||
executables = ["apache2ctl"]
|
||||
dependencies = []
|
||||
startup = ["chown www-data /var/lock/apache2", "apache2ctl start"]
|
||||
validate = ["pidof apache2"]
|
||||
shutdown = ["apache2ctl stop"]
|
||||
validation_mode = ConfigServiceMode.BLOCKING
|
||||
default_configs = []
|
||||
modes = {}
|
||||
|
||||
def data(self) -> Dict[str, Any]:
|
||||
interfaces = []
|
||||
for ifc in self.node.netifs():
|
||||
if getattr(ifc, "control", False):
|
||||
continue
|
||||
interfaces.append(ifc)
|
||||
return dict(interfaces=interfaces)
|
102
daemon/core/configservices/utilservices/templates/apache2.conf
Normal file
102
daemon/core/configservices/utilservices/templates/apache2.conf
Normal file
|
@ -0,0 +1,102 @@
|
|||
# apache2.conf generated by utility.py:HttpService
|
||||
Mutex file:$APACHE_LOCK_DIR default
|
||||
|
||||
PidFile $APACHE_PID_FILE
|
||||
Timeout 300
|
||||
KeepAlive On
|
||||
MaxKeepAliveRequests 100
|
||||
KeepAliveTimeout 5
|
||||
|
||||
LoadModule mpm_worker_module /usr/lib/apache2/modules/mod_mpm_worker.so
|
||||
|
||||
<IfModule mpm_prefork_module>
|
||||
StartServers 5
|
||||
MinSpareServers 5
|
||||
MaxSpareServers 10
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
<IfModule mpm_worker_module>
|
||||
StartServers 2
|
||||
MinSpareThreads 25
|
||||
MaxSpareThreads 75
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
<IfModule mpm_event_module>
|
||||
StartServers 2
|
||||
MinSpareThreads 25
|
||||
MaxSpareThreads 75
|
||||
ThreadLimit 64
|
||||
ThreadsPerChild 25
|
||||
MaxClients 150
|
||||
MaxRequestsPerChild 0
|
||||
</IfModule>
|
||||
|
||||
User $APACHE_RUN_USER
|
||||
Group $APACHE_RUN_GROUP
|
||||
|
||||
AccessFileName .htaccess
|
||||
|
||||
<Files ~ "^\\.ht">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
DefaultType None
|
||||
|
||||
HostnameLookups Off
|
||||
|
||||
ErrorLog $APACHE_LOG_DIR/error.log
|
||||
LogLevel warn
|
||||
|
||||
#Include mods-enabled/*.load
|
||||
#Include mods-enabled/*.conf
|
||||
LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
|
||||
LoadModule auth_basic_module /usr/lib/apache2/modules/mod_auth_basic.so
|
||||
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
|
||||
LoadModule authz_host_module /usr/lib/apache2/modules/mod_authz_host.so
|
||||
LoadModule authz_user_module /usr/lib/apache2/modules/mod_authz_user.so
|
||||
LoadModule autoindex_module /usr/lib/apache2/modules/mod_autoindex.so
|
||||
LoadModule dir_module /usr/lib/apache2/modules/mod_dir.so
|
||||
LoadModule env_module /usr/lib/apache2/modules/mod_env.so
|
||||
|
||||
NameVirtualHost *:80
|
||||
Listen 80
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
LogFormat "%v:%p %h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" vhost_combined
|
||||
LogFormat "%h %l %u %t \\"%r\\" %>s %O \\"%{Referer}i\\" \\"%{User-Agent}i\\"" combined
|
||||
LogFormat "%h %l %u %t \\"%r\\" %>s %O" common
|
||||
LogFormat "%{Referer}i -> %U" referer
|
||||
LogFormat "%{User-agent}i" agent
|
||||
|
||||
ServerTokens OS
|
||||
ServerSignature On
|
||||
TraceEnable Off
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
</Directory>
|
||||
<Directory /var/www/>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
ErrorLog $APACHE_LOG_DIR/error.log
|
||||
LogLevel warn
|
||||
CustomLog $APACHE_LOG_DIR/access.log combined
|
||||
</VirtualHost>
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DefaultMulticastRoute service (utility.py)
|
||||
# the first interface is chosen below; please change it as needed
|
||||
ip route add 224.0.0.0/4 dev ${ifname}
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DefaultRoute service
|
||||
% for address in addresses:
|
||||
ip route add default via ${address}
|
||||
% endfor
|
22
daemon/core/configservices/utilservices/templates/dhcpd.conf
Normal file
22
daemon/core/configservices/utilservices/templates/dhcpd.conf
Normal file
|
@ -0,0 +1,22 @@
|
|||
# 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 subnet, netmask, rangelow, rangehigh, addr in subnets:
|
||||
subnet ${subnet} netmask ${netmask} {
|
||||
pool {
|
||||
range ${rangelow} ${rangehigh};
|
||||
default-lease-time 600;
|
||||
option routers ${addr};
|
||||
}
|
||||
}
|
||||
% endfor
|
10
daemon/core/configservices/utilservices/templates/envvars
Normal file
10
daemon/core/configservices/utilservices/templates/envvars
Normal file
|
@ -0,0 +1,10 @@
|
|||
# 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
|
13
daemon/core/configservices/utilservices/templates/index.html
Normal file
13
daemon/core/configservices/utilservices/templates/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!-- generated by utility.py:HttpService -->
|
||||
<html>
|
||||
<body>
|
||||
<h1>${node.name} web server</h1>
|
||||
<p>This is the default web page for this server.</p>
|
||||
<p>The web server software is running but no content has been added, yet.</p>
|
||||
<ul>
|
||||
% for ifc in interfaces:
|
||||
<li>${ifc.name} - ${ifc.addrlist}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
#!/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
|
||||
# setup forwarding for node interfaces
|
||||
% for devname in devnames:
|
||||
sysctl -w net.ipv4.conf.${devname}.forwarding=1
|
||||
sysctl -w net.ipv4.conf.${devname}.send_redirects=0
|
||||
sysctl -w net.ipv4.conf.${devname}.rp_filter=0
|
||||
% endfor
|
11
daemon/core/configservices/utilservices/templates/pcap.sh
Normal file
11
daemon/core/configservices/utilservices/templates/pcap.sh
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
# set tcpdump options here (see 'man tcpdump' for help)
|
||||
# (-s snap length, -C limit pcap file length, -n disable name resolution)
|
||||
if [ "x$1" = "xstart" ]; then
|
||||
% for ifname in ifnames:
|
||||
tcpdump -s 12288 -C 10 -n -w ${node.name}.${ifname}.pcap -i ${ifname} < /dev/null &
|
||||
% endfor
|
||||
elif [ "x$1" = "xstop" ]; then
|
||||
mkdir -p $SESSION_DIR/pcap
|
||||
mv *.pcap $SESSION_DIR/pcap
|
||||
fi;
|
19
daemon/core/configservices/utilservices/templates/radvd.conf
Normal file
19
daemon/core/configservices/utilservices/templates/radvd.conf
Normal file
|
@ -0,0 +1,19 @@
|
|||
# auto-generated by RADVD service (utility.py)
|
||||
% for ifname, prefixes in values:
|
||||
interface ${ifname}
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
MinRtrAdvInterval 3;
|
||||
MaxRtrAdvInterval 10;
|
||||
AdvDefaultPreference low;
|
||||
AdvHomeAgentFlag off;
|
||||
% for prefix in prefixes:
|
||||
prefix ${prefix}
|
||||
{
|
||||
AdvOnLink on;
|
||||
AdvAutonomous on;
|
||||
AdvRouterAddr on;
|
||||
};
|
||||
% endfor
|
||||
};
|
||||
% endfor
|
|
@ -0,0 +1,37 @@
|
|||
# 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
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
echo 00001 > /var/spool/cron/atjobs/.SEQ
|
||||
chown -R daemon /var/spool/cron/*
|
||||
chmod -R 700 /var/spool/cron/*
|
||||
atd
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by DHCPClient service (utility.py)
|
||||
# uncomment this mkdir line and symlink line to enable client-side DNS\n# resolution based on the DHCP server response.
|
||||
#mkdir -p /var/run/resolvconf/interface
|
||||
% for ifname in ifnames:
|
||||
#ln -s /var/run/resolvconf/interface/${ifname}.dhclient /var/run/resolvconf/resolv.conf
|
||||
dhclient -nw -pf /var/run/dhclient-${ifname}.pid -lf /var/run/dhclient-${ifname}.lease ${ifname}
|
||||
% endfor
|
|
@ -0,0 +1,6 @@
|
|||
#!/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
|
||||
$(which sshd) -f ${sshcfgdir}/sshd_config
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
# auto-generated by StaticRoute service (utility.py)
|
||||
# NOTE: this service must be customized to be of any use
|
||||
# Below are samples that you can uncomment and edit.
|
||||
% for dest, addr in routes:
|
||||
#ip route add ${dest} via ${addr}
|
||||
% endfor
|
|
@ -0,0 +1,12 @@
|
|||
# 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
|
|
@ -21,7 +21,6 @@ class EmaneBypassModel(emanemodel.EmaneModel):
|
|||
_id="none",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["True", "False"],
|
||||
label="There are no parameters for the bypass model.",
|
||||
)
|
||||
]
|
||||
|
|
|
@ -6,6 +6,8 @@ import sys
|
|||
from typing import Mapping, Type
|
||||
|
||||
import core.services
|
||||
from core import configservices
|
||||
from core.configservice.manager import ConfigServiceManager
|
||||
from core.emulator.session import Session
|
||||
from core.services.coreservices import ServiceManager
|
||||
|
||||
|
@ -55,6 +57,11 @@ class CoreEmu:
|
|||
self.service_errors = []
|
||||
self.load_services()
|
||||
|
||||
# config services
|
||||
self.service_manager = ConfigServiceManager()
|
||||
config_services_path = os.path.abspath(os.path.dirname(configservices.__file__))
|
||||
self.service_manager.load(config_services_path)
|
||||
|
||||
# catch exit event
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
|
@ -97,6 +104,7 @@ class CoreEmu:
|
|||
while _id in self.sessions:
|
||||
_id += 1
|
||||
session = _cls(_id, config=self.config)
|
||||
session.service_manager = self.service_manager
|
||||
logging.info("created session: %s", _id)
|
||||
self.sessions[_id] = session
|
||||
return session
|
||||
|
|
|
@ -75,6 +75,7 @@ class NodeOptions:
|
|||
self.icon = None
|
||||
self.opaque = None
|
||||
self.services = []
|
||||
self.config_services = []
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.lat = None
|
||||
|
|
|
@ -161,6 +161,9 @@ class Session:
|
|||
"host": ("DefaultRoute", "SSH"),
|
||||
}
|
||||
|
||||
# config services
|
||||
self.service_manager = None
|
||||
|
||||
@classmethod
|
||||
def get_node_class(cls, _type: NodeTypes) -> Type[NodeBase]:
|
||||
"""
|
||||
|
@ -726,6 +729,12 @@ class Session:
|
|||
logging.debug("set node type: %s", node.type)
|
||||
self.services.add_services(node, node.type, options.services)
|
||||
|
||||
# add config services
|
||||
logging.info("setting node config services: %s", options.config_services)
|
||||
for name in options.config_services:
|
||||
service_class = self.service_manager.get_service(name)
|
||||
node.add_config_service(service_class)
|
||||
|
||||
# ensure default emane configuration
|
||||
if isinstance(node, EmaneNet) and options.emane:
|
||||
self.emane.set_model_config(_id, options.emane)
|
||||
|
@ -1602,6 +1611,7 @@ class Session:
|
|||
logging.info("booting node(%s): %s", node.name, [x.name for x in node.services])
|
||||
self.add_remove_control_interface(node=node, remove=False)
|
||||
self.services.boot_services(node)
|
||||
node.start_config_services()
|
||||
|
||||
def boot_nodes(self) -> List[Exception]:
|
||||
"""
|
||||
|
|
|
@ -36,21 +36,18 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions):
|
|||
_id="enablerj45",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
options=["On", "Off"],
|
||||
label="Enable RJ45s",
|
||||
),
|
||||
Configuration(
|
||||
_id="preservedir",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["On", "Off"],
|
||||
label="Preserve session dir",
|
||||
),
|
||||
Configuration(
|
||||
_id="enablesdt",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="0",
|
||||
options=["On", "Off"],
|
||||
label="Enable SDT3D output",
|
||||
),
|
||||
Configuration(
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Dict, List
|
|||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import client, core_pb2
|
||||
from core.api.grpc import client, common_pb2, configservices_pb2, core_pb2
|
||||
from core.gui import appconfig
|
||||
from core.gui.dialogs.mobilityplayer import MobilityPlayer
|
||||
from core.gui.dialogs.sessions import SessionsDialog
|
||||
|
@ -74,6 +74,8 @@ class CoreClient:
|
|||
self.app = app
|
||||
self.master = app.master
|
||||
self.services = {}
|
||||
self.config_services_groups = {}
|
||||
self.config_services = {}
|
||||
self.default_services = {}
|
||||
self.emane_models = []
|
||||
self.observer = None
|
||||
|
@ -99,6 +101,7 @@ class CoreClient:
|
|||
self.emane_model_configs = {}
|
||||
self.emane_config = None
|
||||
self.service_configs = {}
|
||||
self.config_service_configs = {}
|
||||
self.file_configs = {}
|
||||
self.mobility_players = {}
|
||||
self.handling_throughputs = None
|
||||
|
@ -307,6 +310,18 @@ class CoreClient:
|
|||
data = config.files[file_name]
|
||||
files[file_name] = data
|
||||
|
||||
# get config service configurations
|
||||
response = self.client.get_node_config_service_configs(self.session_id)
|
||||
for config in response.configs:
|
||||
node_configs = self.config_service_configs.setdefault(
|
||||
config.node_id, {}
|
||||
)
|
||||
service_config = node_configs.setdefault(config.name, {})
|
||||
if config.templates:
|
||||
service_config["templates"] = config.templates
|
||||
if config.config:
|
||||
service_config["config"] = config.config
|
||||
|
||||
# draw session
|
||||
self.app.canvas.reset_and_redraw(session)
|
||||
|
||||
|
@ -427,6 +442,15 @@ class CoreClient:
|
|||
group_services = self.services.setdefault(service.group, set())
|
||||
group_services.add(service.name)
|
||||
|
||||
# get config service informations
|
||||
response = self.client.get_config_services()
|
||||
for service in response.services:
|
||||
self.config_services[service.name] = service
|
||||
group_services = self.config_services_groups.setdefault(
|
||||
service.group, set()
|
||||
)
|
||||
group_services.add(service.name)
|
||||
|
||||
# if there are no sessions, create a new session, else join a session
|
||||
response = self.client.get_sessions()
|
||||
logging.info("current sessions: %s", response)
|
||||
|
@ -464,6 +488,7 @@ class CoreClient:
|
|||
asymmetric_links = [
|
||||
x.asymmetric_link for x in self.links.values() if x.asymmetric_link
|
||||
]
|
||||
config_service_configs = self.get_config_service_configs_proto()
|
||||
if self.emane_config:
|
||||
emane_config = {x: self.emane_config[x].value for x in self.emane_config}
|
||||
else:
|
||||
|
@ -484,6 +509,7 @@ class CoreClient:
|
|||
service_configs,
|
||||
file_configs,
|
||||
asymmetric_links,
|
||||
config_service_configs,
|
||||
)
|
||||
logging.debug(
|
||||
"start session(%s), result: %s", self.session_id, response.result
|
||||
|
@ -878,18 +904,34 @@ class CoreClient:
|
|||
configs.append(config_proto)
|
||||
return configs
|
||||
|
||||
def get_config_service_configs_proto(
|
||||
self
|
||||
) -> List[configservices_pb2.ConfigServiceConfig]:
|
||||
config_service_protos = []
|
||||
for node_id, node_config in self.config_service_configs.items():
|
||||
for name, service_config in node_config.items():
|
||||
config = service_config.get("config", {})
|
||||
config_proto = configservices_pb2.ConfigServiceConfig(
|
||||
node_id=node_id,
|
||||
name=name,
|
||||
templates=service_config["templates"],
|
||||
config=config,
|
||||
)
|
||||
config_service_protos.append(config_proto)
|
||||
return config_service_protos
|
||||
|
||||
def run(self, node_id: int) -> str:
|
||||
logging.info("running node(%s) cmd: %s", node_id, self.observer)
|
||||
return self.client.node_command(self.session_id, node_id, self.observer).output
|
||||
|
||||
def get_wlan_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]:
|
||||
def get_wlan_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.wlan_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_wlan_config(self.session_id, node_id)
|
||||
config = response.config
|
||||
return config
|
||||
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, core_pb2.ConfigOption]:
|
||||
def get_mobility_config(self, node_id: int) -> Dict[str, common_pb2.ConfigOption]:
|
||||
config = self.mobility_configs.get(node_id)
|
||||
if not config:
|
||||
response = self.client.get_mobility_config(self.session_id, node_id)
|
||||
|
@ -898,7 +940,7 @@ class CoreClient:
|
|||
|
||||
def get_emane_model_config(
|
||||
self, node_id: int, model: str, interface: int = None
|
||||
) -> Dict[str, core_pb2.ConfigOption]:
|
||||
) -> Dict[str, common_pb2.ConfigOption]:
|
||||
logging.info("getting emane model config: %s %s %s", node_id, model, interface)
|
||||
config = self.emane_model_configs.get((node_id, model, interface))
|
||||
if not config:
|
||||
|
@ -914,7 +956,7 @@ class CoreClient:
|
|||
self,
|
||||
node_id: int,
|
||||
model: str,
|
||||
config: Dict[str, core_pb2.ConfigOption],
|
||||
config: Dict[str, common_pb2.ConfigOption],
|
||||
interface: int = None,
|
||||
):
|
||||
logging.info("setting emane model config: %s %s %s", node_id, model, interface)
|
||||
|
|
381
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
381
daemon/core/gui/dialogs/configserviceconfig.py
Normal file
|
@ -0,0 +1,381 @@
|
|||
"""
|
||||
Service configuration dialog
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
import grpc
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
|
||||
|
||||
class ConfigServiceConfigDialog(Dialog):
|
||||
def __init__(
|
||||
self, master: Any, app: "Application", service_name: str, node_id: int
|
||||
):
|
||||
title = f"{service_name} Config Service"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.master = master
|
||||
self.app = app
|
||||
self.core = app.core
|
||||
self.node_id = node_id
|
||||
self.service_name = service_name
|
||||
self.service_configs = app.core.config_service_configs
|
||||
|
||||
self.radiovar = tk.IntVar()
|
||||
self.radiovar.set(2)
|
||||
self.directories = []
|
||||
self.templates = []
|
||||
self.dependencies = []
|
||||
self.executables = []
|
||||
self.startup_commands = []
|
||||
self.validation_commands = []
|
||||
self.shutdown_commands = []
|
||||
self.default_startup = []
|
||||
self.default_validate = []
|
||||
self.default_shutdown = []
|
||||
self.validation_mode = None
|
||||
self.validation_time = None
|
||||
self.validation_period = tk.StringVar()
|
||||
self.modes = []
|
||||
self.mode_configs = {}
|
||||
|
||||
self.notebook = None
|
||||
self.templates_combobox = None
|
||||
self.modes_combobox = None
|
||||
self.startup_commands_listbox = None
|
||||
self.shutdown_commands_listbox = None
|
||||
self.validate_commands_listbox = None
|
||||
self.validation_time_entry = None
|
||||
self.validation_mode_entry = None
|
||||
self.template_text = None
|
||||
self.validation_period_entry = None
|
||||
self.original_service_files = {}
|
||||
self.temp_service_files = {}
|
||||
self.modified_files = set()
|
||||
self.config_frame = None
|
||||
self.default_config = None
|
||||
self.config = None
|
||||
self.load()
|
||||
self.draw()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
self.core.create_nodes_and_links()
|
||||
service = self.core.config_services[self.service_name]
|
||||
self.dependencies = service.dependencies[:]
|
||||
self.executables = service.executables[:]
|
||||
self.directories = service.directories[:]
|
||||
self.templates = service.files[:]
|
||||
self.startup_commands = service.startup[:]
|
||||
self.validation_commands = service.validate[:]
|
||||
self.shutdown_commands = service.shutdown[:]
|
||||
self.validation_mode = service.validation_mode
|
||||
self.validation_time = service.validation_timer
|
||||
self.validation_period.set(service.validation_period)
|
||||
|
||||
response = self.core.client.get_config_service_defaults(self.service_name)
|
||||
self.original_service_files = response.templates
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
|
||||
self.modes = sorted(x.name for x in response.modes)
|
||||
self.mode_configs = {x.name: x.config for x in response.modes}
|
||||
|
||||
node_configs = self.service_configs.get(self.node_id, {})
|
||||
service_config = node_configs.get(self.service_name, {})
|
||||
|
||||
self.config = response.config
|
||||
self.default_config = {x.name: x.value for x in self.config.values()}
|
||||
custom_config = service_config.get("config")
|
||||
if custom_config:
|
||||
for key, value in custom_config.items():
|
||||
self.config[key].value = value
|
||||
logging.info("default config: %s", self.default_config)
|
||||
|
||||
custom_templates = service_config.get("templates", {})
|
||||
for file, data in custom_templates.items():
|
||||
self.modified_files.add(file)
|
||||
self.temp_service_files[file] = data
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
# draw notebook
|
||||
self.notebook = ttk.Notebook(self.top)
|
||||
self.notebook.grid(sticky="nsew", pady=PADY)
|
||||
self.draw_tab_files()
|
||||
if self.config:
|
||||
self.draw_tab_config()
|
||||
self.draw_tab_startstop()
|
||||
self.draw_tab_validation()
|
||||
self.draw_buttons()
|
||||
|
||||
def draw_tab_files(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Directories/Files")
|
||||
|
||||
label = ttk.Label(
|
||||
tab, text="Directories and templates that will be used for this service."
|
||||
)
|
||||
label.grid(pady=PADY)
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Directories")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
directories_combobox = ttk.Combobox(
|
||||
frame, values=self.directories, state="readonly"
|
||||
)
|
||||
directories_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
if self.directories:
|
||||
directories_combobox.current(0)
|
||||
|
||||
label = ttk.Label(frame, text="Templates")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
self.templates_combobox = ttk.Combobox(
|
||||
frame, values=self.templates, state="readonly"
|
||||
)
|
||||
self.templates_combobox.bind(
|
||||
"<<ComboboxSelected>>", self.handle_template_changed
|
||||
)
|
||||
self.templates_combobox.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
self.template_text = CodeText(tab)
|
||||
self.template_text.grid(sticky="nsew")
|
||||
tab.rowconfigure(self.template_text.grid_info()["row"], weight=1)
|
||||
if self.templates:
|
||||
self.templates_combobox.current(0)
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert(
|
||||
"end", self.temp_service_files[self.templates[0]]
|
||||
)
|
||||
self.template_text.text.bind("<FocusOut>", self.update_template_file_data)
|
||||
|
||||
def draw_tab_config(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Configuration")
|
||||
|
||||
if self.modes:
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
label = ttk.Label(frame, text="Modes")
|
||||
label.grid(row=0, column=0, padx=PADX)
|
||||
self.modes_combobox = ttk.Combobox(
|
||||
frame, values=self.modes, state="readonly"
|
||||
)
|
||||
self.modes_combobox.bind("<<ComboboxSelected>>", self.handle_mode_changed)
|
||||
self.modes_combobox.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
logging.info("config service config: %s", self.config)
|
||||
self.config_frame = ConfigFrame(tab, self.app, self.config)
|
||||
self.config_frame.draw_config()
|
||||
self.config_frame.grid(sticky="nsew", pady=PADY)
|
||||
tab.rowconfigure(self.config_frame.grid_info()["row"], weight=1)
|
||||
|
||||
def draw_tab_startstop(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="nsew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
tab.rowconfigure(i, weight=1)
|
||||
self.notebook.add(tab, text="Startup/Shutdown")
|
||||
commands = []
|
||||
# tab 3
|
||||
for i in range(3):
|
||||
label_frame = None
|
||||
if i == 0:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Startup Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.startup_commands
|
||||
elif i == 1:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Shutdown Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.shutdown_commands
|
||||
elif i == 2:
|
||||
label_frame = ttk.LabelFrame(
|
||||
tab, text="Validation Commands", padding=FRAME_PAD
|
||||
)
|
||||
commands = self.validation_commands
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
for command in commands:
|
||||
listbox_scroll.listbox.insert("end", command)
|
||||
listbox_scroll.listbox.config(height=4)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
if i == 0:
|
||||
self.startup_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 1:
|
||||
self.shutdown_commands_listbox = listbox_scroll.listbox
|
||||
elif i == 2:
|
||||
self.validate_commands_listbox = listbox_scroll.listbox
|
||||
|
||||
def draw_tab_validation(self):
|
||||
tab = ttk.Frame(self.notebook, padding=FRAME_PAD)
|
||||
tab.grid(sticky="ew")
|
||||
tab.columnconfigure(0, weight=1)
|
||||
self.notebook.add(tab, text="Validation", sticky="nsew")
|
||||
|
||||
frame = ttk.Frame(tab)
|
||||
frame.grid(sticky="ew", pady=PADY)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Time")
|
||||
label.grid(row=0, column=0, sticky="w", padx=PADX)
|
||||
self.validation_time_entry = ttk.Entry(frame)
|
||||
self.validation_time_entry.insert("end", self.validation_time)
|
||||
self.validation_time_entry.config(state=tk.DISABLED)
|
||||
self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Mode")
|
||||
label.grid(row=1, column=0, sticky="w", padx=PADX)
|
||||
if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING:
|
||||
mode = "BLOCKING"
|
||||
elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING:
|
||||
mode = "NON_BLOCKING"
|
||||
else:
|
||||
mode = "TIMER"
|
||||
self.validation_mode_entry = ttk.Entry(
|
||||
frame, textvariable=tk.StringVar(value=mode)
|
||||
)
|
||||
self.validation_mode_entry.insert("end", mode)
|
||||
self.validation_mode_entry.config(state=tk.DISABLED)
|
||||
self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label = ttk.Label(frame, text="Validation Period")
|
||||
label.grid(row=2, column=0, sticky="w", padx=PADX)
|
||||
self.validation_period_entry = ttk.Entry(
|
||||
frame, state=tk.DISABLED, textvariable=self.validation_period
|
||||
)
|
||||
self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for executable in self.executables:
|
||||
listbox_scroll.listbox.insert("end", executable)
|
||||
|
||||
label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD)
|
||||
label_frame.grid(sticky="nsew", pady=PADY)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
listbox_scroll = ListboxScroll(label_frame)
|
||||
listbox_scroll.grid(sticky="nsew")
|
||||
tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1)
|
||||
for dependency in self.dependencies:
|
||||
listbox_scroll.listbox.insert("end", dependency)
|
||||
|
||||
def draw_buttons(self):
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(sticky="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Apply", command=self.click_apply)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Defaults", command=self.click_defaults)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Copy...", command=self.click_copy)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.destroy)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
def click_apply(self):
|
||||
current_listbox = self.master.current.listbox
|
||||
if not self.is_custom():
|
||||
if self.node_id in self.service_configs:
|
||||
self.service_configs[self.node_id].pop(self.service_name, None)
|
||||
current_listbox.itemconfig(current_listbox.curselection()[0], bg="")
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
node_config = self.service_configs.setdefault(self.node_id, {})
|
||||
service_config = node_config.setdefault(self.service_name, {})
|
||||
if self.config_frame:
|
||||
self.config_frame.parse_config()
|
||||
service_config["config"] = {
|
||||
x.name: x.value for x in self.config.values()
|
||||
}
|
||||
templates_config = service_config.setdefault("templates", {})
|
||||
for file in self.modified_files:
|
||||
templates_config[file] = self.temp_service_files[file]
|
||||
all_current = current_listbox.get(0, tk.END)
|
||||
current_listbox.itemconfig(all_current.index(self.service_name), bg="green")
|
||||
except grpc.RpcError as e:
|
||||
show_grpc_error(e)
|
||||
self.destroy()
|
||||
|
||||
def handle_template_changed(self, event: tk.Event):
|
||||
template = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[template])
|
||||
|
||||
def handle_mode_changed(self, event: tk.Event):
|
||||
mode = self.modes_combobox.get()
|
||||
config = self.mode_configs[mode]
|
||||
logging.info("mode config: %s", config)
|
||||
self.config_frame.set_values(config)
|
||||
|
||||
def update_template_file_data(self, event: tk.Event):
|
||||
scrolledtext = event.widget
|
||||
template = self.templates_combobox.get()
|
||||
self.temp_service_files[template] = scrolledtext.get(1.0, "end")
|
||||
if self.temp_service_files[template] != self.original_service_files[template]:
|
||||
self.modified_files.add(template)
|
||||
else:
|
||||
self.modified_files.discard(template)
|
||||
|
||||
def is_custom(self):
|
||||
has_custom_templates = len(self.modified_files) > 0
|
||||
has_custom_config = False
|
||||
if self.config_frame:
|
||||
current = self.config_frame.parse_config()
|
||||
has_custom_config = self.default_config != current
|
||||
return has_custom_templates or has_custom_config
|
||||
|
||||
def click_defaults(self):
|
||||
if self.node_id in self.service_configs:
|
||||
node_config = self.service_configs.get(self.node_id, {})
|
||||
node_config.pop(self.service_name, None)
|
||||
self.temp_service_files = dict(self.original_service_files)
|
||||
filename = self.templates_combobox.get()
|
||||
self.template_text.text.delete(1.0, "end")
|
||||
self.template_text.text.insert("end", self.temp_service_files[filename])
|
||||
if self.config_frame:
|
||||
logging.info("resetting defaults: %s", self.default_config)
|
||||
self.config_frame.set_values(self.default_config)
|
||||
|
||||
def click_copy(self):
|
||||
pass
|
||||
|
||||
def append_commands(
|
||||
self, commands: List[str], listbox: tk.Listbox, to_add: List[str]
|
||||
):
|
||||
for cmd in to_add:
|
||||
commands.append(cmd)
|
||||
listbox.insert(tk.END, cmd)
|
161
daemon/core/gui/dialogs/nodeconfigservice.py
Normal file
161
daemon/core/gui/dialogs/nodeconfigservice.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
"""
|
||||
core node services
|
||||
"""
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import TYPE_CHECKING, Any, Set
|
||||
|
||||
from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog
|
||||
from core.gui.dialogs.dialog import Dialog
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
from core.gui.widgets import CheckboxList, ListboxScroll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.gui.app import Application
|
||||
from core.gui.graph.node import CanvasNode
|
||||
|
||||
|
||||
class NodeConfigServiceDialog(Dialog):
|
||||
def __init__(
|
||||
self,
|
||||
master: Any,
|
||||
app: "Application",
|
||||
canvas_node: "CanvasNode",
|
||||
services: Set[str] = None,
|
||||
):
|
||||
title = f"{canvas_node.core_node.name} Config Services"
|
||||
super().__init__(master, app, title, modal=True)
|
||||
self.app = app
|
||||
self.canvas_node = canvas_node
|
||||
self.node_id = canvas_node.core_node.id
|
||||
self.groups = None
|
||||
self.services = None
|
||||
self.current = None
|
||||
if services is None:
|
||||
services = set(canvas_node.core_node.config_services)
|
||||
self.current_services = services
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
self.top.columnconfigure(0, weight=1)
|
||||
self.top.rowconfigure(0, weight=1)
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="nsew", pady=PADY)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
for i in range(3):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=0, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.groups = ListboxScroll(label_frame)
|
||||
self.groups.grid(sticky="nsew")
|
||||
for group in sorted(self.app.core.config_services_groups):
|
||||
self.groups.listbox.insert(tk.END, group)
|
||||
self.groups.listbox.bind("<<ListboxSelect>>", self.handle_group_change)
|
||||
self.groups.listbox.selection_set(0)
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Services")
|
||||
label_frame.grid(row=0, column=1, sticky="nsew")
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
self.services = CheckboxList(
|
||||
label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD
|
||||
)
|
||||
self.services.grid(sticky="nsew")
|
||||
|
||||
label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD)
|
||||
label_frame.grid(row=0, column=2, sticky="nsew")
|
||||
label_frame.rowconfigure(0, weight=1)
|
||||
label_frame.columnconfigure(0, weight=1)
|
||||
self.current = ListboxScroll(label_frame)
|
||||
self.current.grid(sticky="nsew")
|
||||
for service in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, service)
|
||||
if self.is_custom_service(service):
|
||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
|
||||
frame = ttk.Frame(self.top)
|
||||
frame.grid(stick="ew")
|
||||
for i in range(4):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
button = ttk.Button(frame, text="Configure", command=self.click_configure)
|
||||
button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Save", command=self.click_save)
|
||||
button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Remove", command=self.click_remove)
|
||||
button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||
button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
|
||||
button.grid(row=0, column=3, sticky="ew")
|
||||
|
||||
# trigger group change
|
||||
self.groups.listbox.event_generate("<<ListboxSelect>>")
|
||||
|
||||
def handle_group_change(self, event: tk.Event = None):
|
||||
selection = self.groups.listbox.curselection()
|
||||
if selection:
|
||||
index = selection[0]
|
||||
group = self.groups.listbox.get(index)
|
||||
self.services.clear()
|
||||
for name in sorted(self.app.core.config_services_groups[group]):
|
||||
checked = name in self.current_services
|
||||
self.services.add(name, checked)
|
||||
|
||||
def service_clicked(self, name: str, var: tk.IntVar):
|
||||
if var.get() and name not in self.current_services:
|
||||
self.current_services.add(name)
|
||||
elif not var.get() and name in self.current_services:
|
||||
self.current_services.remove(name)
|
||||
self.current.listbox.delete(0, tk.END)
|
||||
for name in sorted(self.current_services):
|
||||
self.current.listbox.insert(tk.END, name)
|
||||
if self.is_custom_service(name):
|
||||
self.current.listbox.itemconfig(tk.END, bg="green")
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
|
||||
def click_configure(self):
|
||||
current_selection = self.current.listbox.curselection()
|
||||
if len(current_selection):
|
||||
dialog = ConfigServiceConfigDialog(
|
||||
master=self,
|
||||
app=self.app,
|
||||
service_name=self.current.listbox.get(current_selection[0]),
|
||||
node_id=self.node_id,
|
||||
)
|
||||
dialog.show()
|
||||
else:
|
||||
messagebox.showinfo(
|
||||
"Node service configuration", "Select a service to configure"
|
||||
)
|
||||
|
||||
def click_save(self):
|
||||
self.canvas_node.core_node.config_services[:] = self.current_services
|
||||
logging.info(
|
||||
"saved node config services: %s", self.canvas_node.core_node.config_services
|
||||
)
|
||||
self.destroy()
|
||||
|
||||
def click_cancel(self):
|
||||
self.current_services = None
|
||||
self.destroy()
|
||||
|
||||
def click_remove(self):
|
||||
cur = self.current.listbox.curselection()
|
||||
if cur:
|
||||
service = self.current.listbox.get(cur[0])
|
||||
self.current.listbox.delete(cur[0])
|
||||
self.current_services.remove(service)
|
||||
for checkbutton in self.services.frame.winfo_children():
|
||||
if checkbutton["text"] == service:
|
||||
checkbutton.invoke()
|
||||
return
|
||||
|
||||
def is_custom_service(self, service: str) -> bool:
|
||||
node_configs = self.app.core.config_service_configs.get(self.node_id, {})
|
||||
service_config = node_configs.get(service)
|
||||
if node_configs and service_config:
|
||||
return True
|
||||
else:
|
||||
return False
|
|
@ -10,6 +10,7 @@ from core.gui import themes
|
|||
from core.gui.dialogs.emaneconfig import EmaneConfigDialog
|
||||
from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
|
||||
from core.gui.dialogs.nodeconfig import NodeConfigDialog
|
||||
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
|
||||
from core.gui.dialogs.nodeservice import NodeServiceDialog
|
||||
from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
||||
from core.gui.errors import show_grpc_error
|
||||
|
@ -180,6 +181,7 @@ class CanvasNode:
|
|||
context.add_command(label="Configure", command=self.show_config)
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
context.add_command(label="Services", state=tk.DISABLED)
|
||||
context.add_command(label="Config Services", state=tk.DISABLED)
|
||||
if is_wlan:
|
||||
context.add_command(label="WLAN Config", command=self.show_wlan_config)
|
||||
if is_wlan and self.core_node.id in self.app.core.mobility_players:
|
||||
|
@ -198,6 +200,9 @@ class CanvasNode:
|
|||
context.add_command(label="Configure", command=self.show_config)
|
||||
if NodeUtils.is_container_node(self.core_node.type):
|
||||
context.add_command(label="Services", command=self.show_services)
|
||||
context.add_command(
|
||||
label="Config Services", command=self.show_config_services
|
||||
)
|
||||
if is_emane:
|
||||
context.add_command(
|
||||
label="EMANE Config", command=self.show_emane_config
|
||||
|
@ -253,6 +258,11 @@ class CanvasNode:
|
|||
dialog = NodeServiceDialog(self.app.master, self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def show_config_services(self):
|
||||
self.canvas.context = None
|
||||
dialog = NodeConfigServiceDialog(self.app.master, self.app, self)
|
||||
dialog.show()
|
||||
|
||||
def has_emane_link(self, interface_id: int) -> core_pb2.Node:
|
||||
result = None
|
||||
for edge in self.edges:
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import PosixPath
|
|||
from tkinter import filedialog, font, ttk
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from core.api.grpc import core_pb2
|
||||
from core.api.grpc import common_pb2, core_pb2
|
||||
from core.gui import themes
|
||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
||||
|
||||
|
@ -74,7 +74,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
self,
|
||||
master: tk.Widget,
|
||||
app: "Application",
|
||||
config: Dict[str, core_pb2.ConfigOption],
|
||||
config: Dict[str, common_pb2.ConfigOption],
|
||||
**kw
|
||||
):
|
||||
super().__init__(master, **kw)
|
||||
|
@ -99,7 +99,7 @@ class ConfigFrame(ttk.Notebook):
|
|||
label.grid(row=index, pady=PADY, padx=PADX, sticky="w")
|
||||
value = tk.StringVar()
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
select = tuple(option.select)
|
||||
select = ("On", "Off")
|
||||
combobox = ttk.Combobox(
|
||||
tab.frame, textvariable=value, values=select, state="readonly"
|
||||
)
|
||||
|
@ -184,6 +184,17 @@ class ConfigFrame(ttk.Notebook):
|
|||
|
||||
return {x: self.config[x].value for x in self.config}
|
||||
|
||||
def set_values(self, config: Dict[str, str]) -> None:
|
||||
for name, data in config.items():
|
||||
option = self.config[name]
|
||||
value = self.values[name]
|
||||
if option.type == core_pb2.ConfigOptionType.BOOL:
|
||||
if data == "1":
|
||||
data = "On"
|
||||
else:
|
||||
data = "Off"
|
||||
value.set(data)
|
||||
|
||||
|
||||
class ListboxScroll(ttk.Frame):
|
||||
def __init__(self, master: tk.Widget = None, **kw):
|
||||
|
|
|
@ -906,11 +906,7 @@ class Ns2ScriptedMobility(WayPointMobility):
|
|||
label="refresh time (ms)",
|
||||
),
|
||||
Configuration(
|
||||
_id="loop",
|
||||
_type=ConfigDataTypes.BOOL,
|
||||
default="1",
|
||||
options=["On", "Off"],
|
||||
label="loop",
|
||||
_id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop"
|
||||
),
|
||||
Configuration(
|
||||
_id="autostart",
|
||||
|
|
|
@ -6,15 +6,16 @@ import logging
|
|||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||
|
||||
import netaddr
|
||||
|
||||
from core import utils
|
||||
from core.configservice.dependencies import ConfigServiceDependencies
|
||||
from core.constants import MOUNT_BIN, VNODED_BIN
|
||||
from core.emulator.data import LinkData, NodeData
|
||||
from core.emulator.enumerations import LinkTypes, NodeTypes
|
||||
from core.errors import CoreCommandError
|
||||
from core.errors import CoreCommandError, CoreError
|
||||
from core.nodes import client
|
||||
from core.nodes.interface import CoreInterface, TunTap, Veth
|
||||
from core.nodes.netclient import LinuxNetClient, get_net_client
|
||||
|
@ -22,6 +23,9 @@ from core.nodes.netclient import LinuxNetClient, get_net_client
|
|||
if TYPE_CHECKING:
|
||||
from core.emulator.distributed import DistributedServer
|
||||
from core.emulator.session import Session
|
||||
from core.configservice.base import ConfigService
|
||||
|
||||
ConfigServiceType = Type[ConfigService]
|
||||
|
||||
_DEFAULT_MTU = 1500
|
||||
|
||||
|
@ -277,9 +281,47 @@ class CoreNodeBase(NodeBase):
|
|||
"""
|
||||
super().__init__(session, _id, name, start, server)
|
||||
self.services = []
|
||||
self.config_services = {}
|
||||
self.nodedir = None
|
||||
self.tmpnodedir = False
|
||||
|
||||
def add_config_service(self, service_class: "ConfigServiceType") -> None:
|
||||
"""
|
||||
Adds a configuration service to the node.
|
||||
|
||||
:param service_class: configuration service class to assign to node
|
||||
:return: nothing
|
||||
"""
|
||||
name = service_class.name
|
||||
if name in self.config_services:
|
||||
raise CoreError(f"node({self.name}) already has service({name})")
|
||||
self.config_services[name] = service_class(self)
|
||||
|
||||
def set_service_config(self, name: str, data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Sets configuration service custom config data.
|
||||
|
||||
:param name: name of configuration service
|
||||
:param data: custom config data to set
|
||||
:return: nothing
|
||||
"""
|
||||
service = self.config_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:
|
||||
"""
|
||||
Determins startup paths and starts configuration services, based on their
|
||||
dependency chains.
|
||||
|
||||
:return: nothing
|
||||
"""
|
||||
startup_paths = ConfigServiceDependencies(self.config_services).startup_paths()
|
||||
for startup_path in startup_paths:
|
||||
for service in startup_path:
|
||||
service.start()
|
||||
|
||||
def makenodedir(self) -> None:
|
||||
"""
|
||||
Create the node directory.
|
||||
|
|
|
@ -9,7 +9,7 @@ from core.emane.nodes import EmaneNet
|
|||
from core.emulator.data import LinkData
|
||||
from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
|
||||
from core.emulator.enumerations import NodeTypes
|
||||
from core.nodes.base import CoreNetworkBase, NodeBase
|
||||
from core.nodes.base import CoreNetworkBase, CoreNodeBase, NodeBase
|
||||
from core.nodes.network import CtrlNet
|
||||
from core.services.coreservices import CoreService
|
||||
|
||||
|
@ -219,10 +219,15 @@ class DeviceElement(NodeElement):
|
|||
service_elements = etree.Element("services")
|
||||
for service in self.node.services:
|
||||
etree.SubElement(service_elements, "service", name=service.name)
|
||||
|
||||
if service_elements.getchildren():
|
||||
self.element.append(service_elements)
|
||||
|
||||
config_service_elements = etree.Element("configservices")
|
||||
for name, service in self.node.config_services.items():
|
||||
etree.SubElement(config_service_elements, "service", name=name)
|
||||
if config_service_elements.getchildren():
|
||||
self.element.append(config_service_elements)
|
||||
|
||||
|
||||
class NetworkElement(NodeElement):
|
||||
def __init__(self, session: "Session", node: NodeBase) -> None:
|
||||
|
@ -261,6 +266,7 @@ class CoreXmlWriter:
|
|||
self.write_mobility_configs()
|
||||
self.write_emane_configs()
|
||||
self.write_service_configs()
|
||||
self.write_configservice_configs()
|
||||
self.write_session_origin()
|
||||
self.write_session_hooks()
|
||||
self.write_session_options()
|
||||
|
@ -399,6 +405,32 @@ class CoreXmlWriter:
|
|||
if service_configurations.getchildren():
|
||||
self.scenario.append(service_configurations)
|
||||
|
||||
def write_configservice_configs(self) -> None:
|
||||
service_configurations = etree.Element("configservice_configurations")
|
||||
for node in self.session.nodes.values():
|
||||
if not isinstance(node, CoreNodeBase):
|
||||
continue
|
||||
for name, service in node.config_services.items():
|
||||
service_element = etree.SubElement(
|
||||
service_configurations, "service", name=name
|
||||
)
|
||||
add_attribute(service_element, "node", node.id)
|
||||
if service.custom_config:
|
||||
configs_element = etree.SubElement(service_element, "configs")
|
||||
for key, value in service.custom_config.items():
|
||||
etree.SubElement(
|
||||
configs_element, "config", key=key, value=value
|
||||
)
|
||||
if service.custom_templates:
|
||||
templates_element = etree.SubElement(service_element, "templates")
|
||||
for template_name, template in service.custom_templates.items():
|
||||
template_element = etree.SubElement(
|
||||
templates_element, "template", name=template_name
|
||||
)
|
||||
template_element.text = etree.CDATA(template)
|
||||
if service_configurations.getchildren():
|
||||
self.scenario.append(service_configurations)
|
||||
|
||||
def write_default_services(self) -> None:
|
||||
node_types = etree.Element("default_services")
|
||||
for node_type in self.session.services.default_services:
|
||||
|
@ -568,6 +600,7 @@ class CoreXmlReader:
|
|||
self.read_mobility_configs()
|
||||
self.read_emane_configs()
|
||||
self.read_nodes()
|
||||
self.read_configservice_configs()
|
||||
self.read_links()
|
||||
|
||||
def read_default_services(self) -> None:
|
||||
|
@ -770,6 +803,12 @@ class CoreXmlReader:
|
|||
if service_elements is not None:
|
||||
options.services = [x.get("name") for x in service_elements.iterchildren()]
|
||||
|
||||
config_service_elements = device_element.find("configservices")
|
||||
if config_service_elements is not None:
|
||||
options.config_services = [
|
||||
x.get("name") for x in config_service_elements.iterchildren()
|
||||
]
|
||||
|
||||
position_element = device_element.find("position")
|
||||
if position_element is not None:
|
||||
x = get_float(position_element, "x")
|
||||
|
@ -812,6 +851,36 @@ class CoreXmlReader:
|
|||
)
|
||||
self.session.add_node(_type=node_type, _id=node_id, options=options)
|
||||
|
||||
def read_configservice_configs(self) -> None:
|
||||
configservice_configs = self.scenario.find("configservice_configurations")
|
||||
if configservice_configs is None:
|
||||
return
|
||||
|
||||
for configservice_element in configservice_configs.iterchildren():
|
||||
name = configservice_element.get("name")
|
||||
node_id = get_int(configservice_element, "node")
|
||||
node = self.session.get_node(node_id)
|
||||
service = node.config_services[name]
|
||||
|
||||
configs_element = configservice_element.find("configs")
|
||||
if configs_element is not None:
|
||||
config = {}
|
||||
for config_element in configs_element.iterchildren():
|
||||
key = config_element.get("key")
|
||||
value = config_element.get("value")
|
||||
config[key] = value
|
||||
service.set_config(config)
|
||||
|
||||
templates_element = configservice_element.find("templates")
|
||||
if templates_element is not None:
|
||||
for template_element in templates_element.iterchildren():
|
||||
name = template_element.get("name")
|
||||
template = template_element.text
|
||||
logging.info(
|
||||
"loading xml template(%s): %s", type(template), template
|
||||
)
|
||||
service.set_template(name, template)
|
||||
|
||||
def read_links(self) -> None:
|
||||
link_elements = self.scenario.find("links")
|
||||
if link_elements is None:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue