diff --git a/daemon/core/config.py b/daemon/core/config.py index 222abf01..1fe32adc 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -4,10 +4,12 @@ Common support for configurable CORE objects. import logging from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Type, Union from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes +from core.errors import CoreConfigError from core.nodes.network import WlanNode if TYPE_CHECKING: @@ -15,62 +17,56 @@ if TYPE_CHECKING: WirelessModelType = Type[WirelessModel] +_BOOL_OPTIONS: Set[str] = {"0", "1"} + +@dataclass class ConfigGroup: """ Defines configuration group tabs used for display by ConfigurationOptions. """ - def __init__(self, name: str, start: int, stop: int) -> None: - """ - Creates a ConfigGroup object. - - :param name: configuration group display name - :param start: configurations start index for this group - :param stop: configurations stop index for this group - """ - self.name: str = name - self.start: int = start - self.stop: int = stop + name: str + start: int + stop: int +@dataclass class Configuration: """ Represents a configuration options. """ - def __init__( - self, - _id: str, - _type: ConfigDataTypes, - label: str = None, - default: str = "", - options: List[str] = None, - ) -> None: - """ - Creates a Configuration object. + id: str + type: ConfigDataTypes + label: str = None + default: str = "" + options: List[str] = field(default_factory=list) - :param _id: unique name for configuration - :param _type: configuration data type - :param label: configuration label for display - :param default: default value for configuration - :param options: list options if this is a configuration with a combobox - """ - self.id: str = _id - self.type: ConfigDataTypes = _type - self.default: str = default - if not options: - options = [] - self.options: List[str] = options - if not label: - label = _id - self.label: str = label - - def __str__(self): - return ( - f"{self.__class__.__name__}(id={self.id}, type={self.type}, " - f"default={self.default}, options={self.options})" - ) + def __post_init__(self) -> None: + self.label = self.label if self.label else self.id + if self.type == ConfigDataTypes.BOOL: + if self.default and self.default not in _BOOL_OPTIONS: + raise CoreConfigError( + f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: " + f"{self.default}" + ) + elif self.type == ConfigDataTypes.FLOAT: + if self.default: + try: + float(self.default) + except ValueError: + raise CoreConfigError( + f"{self.id} is not a valid float: {self.default}" + ) + elif self.type != ConfigDataTypes.STRING: + if self.default: + try: + int(self.default) + except ValueError: + raise CoreConfigError( + f"{self.id} is not a valid int: {self.default}" + ) class ConfigurableOptions: diff --git a/daemon/core/configservices/securityservices/services.py b/daemon/core/configservices/securityservices/services.py index c656f5ca..9da27010 100644 --- a/daemon/core/configservices/securityservices/services.py +++ b/daemon/core/configservices/securityservices/services.py @@ -20,20 +20,20 @@ class VpnClient(ConfigService): validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [ Configuration( - _id="keydir", - _type=ConfigDataTypes.STRING, + id="keydir", + type=ConfigDataTypes.STRING, label="Key Dir", default="/etc/core/keys", ), Configuration( - _id="keyname", - _type=ConfigDataTypes.STRING, + id="keyname", + type=ConfigDataTypes.STRING, label="Key Name", default="client1", ), Configuration( - _id="server", - _type=ConfigDataTypes.STRING, + id="server", + type=ConfigDataTypes.STRING, label="Server", default="10.0.2.10", ), @@ -54,20 +54,20 @@ class VpnServer(ConfigService): validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [ Configuration( - _id="keydir", - _type=ConfigDataTypes.STRING, + id="keydir", + type=ConfigDataTypes.STRING, label="Key Dir", default="/etc/core/keys", ), Configuration( - _id="keyname", - _type=ConfigDataTypes.STRING, + id="keyname", + type=ConfigDataTypes.STRING, label="Key Name", default="server", ), Configuration( - _id="subnet", - _type=ConfigDataTypes.STRING, + id="subnet", + type=ConfigDataTypes.STRING, label="Subnet", default="10.0.200.0", ), diff --git a/daemon/core/configservices/simpleservice.py b/daemon/core/configservices/simpleservice.py index c2e7242f..4370977d 100644 --- a/daemon/core/configservices/simpleservice.py +++ b/daemon/core/configservices/simpleservice.py @@ -17,11 +17,11 @@ class SimpleService(ConfigService): shutdown: List[str] = [] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [ - Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), - Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), + Configuration(id="value1", type=ConfigDataTypes.STRING, label="Text"), + Configuration(id="value2", type=ConfigDataTypes.BOOL, label="Boolean"), Configuration( - _id="value3", - _type=ConfigDataTypes.STRING, + id="value3", + type=ConfigDataTypes.STRING, label="Multiple Choice", options=["value1", "value2", "value3"], ), diff --git a/daemon/core/emane/bypass.py b/daemon/core/emane/bypass.py index 8aabc3f9..aebdde21 100644 --- a/daemon/core/emane/bypass.py +++ b/daemon/core/emane/bypass.py @@ -18,8 +18,8 @@ class EmaneBypassModel(emanemodel.EmaneModel): mac_library: str = "bypassmaclayer" mac_config: List[Configuration] = [ Configuration( - _id="none", - _type=ConfigDataTypes.BOOL, + id="none", + type=ConfigDataTypes.BOOL, default="0", label="There are no parameters for the bypass model.", ) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 5d8e0c07..2eb28c28 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -825,38 +825,38 @@ class EmaneGlobalModel: self.session: "Session" = session self.core_config: List[Configuration] = [ Configuration( - _id="platform_id_start", - _type=ConfigDataTypes.INT32, + id="platform_id_start", + type=ConfigDataTypes.INT32, default="1", label="Starting Platform ID", ), Configuration( - _id="nem_id_start", - _type=ConfigDataTypes.INT32, + id="nem_id_start", + type=ConfigDataTypes.INT32, default="1", label="Starting NEM ID", ), Configuration( - _id="link_enabled", - _type=ConfigDataTypes.BOOL, + id="link_enabled", + type=ConfigDataTypes.BOOL, default="1", label="Enable Links?", ), Configuration( - _id="loss_threshold", - _type=ConfigDataTypes.INT32, + id="loss_threshold", + type=ConfigDataTypes.INT32, default="30", label="Link Loss Threshold (%)", ), Configuration( - _id="link_interval", - _type=ConfigDataTypes.INT32, + id="link_interval", + type=ConfigDataTypes.INT32, default="1", label="Link Check Interval (sec)", ), Configuration( - _id="link_timeout", - _type=ConfigDataTypes.INT32, + id="link_timeout", + type=ConfigDataTypes.INT32, default="4", label="Link Timeout (sec)", ), diff --git a/daemon/core/emane/emanemanifest.py b/daemon/core/emane/emanemanifest.py index 8e09d040..6ada2da7 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -118,8 +118,8 @@ def parse(manifest_path: Path, defaults: Dict[str, str]) -> List[Configuration]: config_descriptions = f"{config_descriptions} file" configuration = Configuration( - _id=config_name, - _type=config_type_value, + id=config_name, + type=config_type_value, default=config_default, options=possible, label=config_descriptions, diff --git a/daemon/core/emane/tdma.py b/daemon/core/emane/tdma.py index 1ddb14ad..35ea55b0 100644 --- a/daemon/core/emane/tdma.py +++ b/daemon/core/emane/tdma.py @@ -35,8 +35,8 @@ class EmaneTdmaModel(emanemodel.EmaneModel): ) super().load(emane_prefix) config_item = Configuration( - _id=cls.schedule_name, - _type=ConfigDataTypes.STRING, + id=cls.schedule_name, + type=ConfigDataTypes.STRING, default=str(cls.default_schedule), label="TDMA schedule file (core)", ) diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index 9b22bcc7..c4099fcd 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -13,51 +13,51 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): name: str = "session" options: List[Configuration] = [ Configuration( - _id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network" + id="controlnet", type=ConfigDataTypes.STRING, label="Control Network" ), Configuration( - _id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0" + id="controlnet0", type=ConfigDataTypes.STRING, label="Control Network 0" ), Configuration( - _id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1" + id="controlnet1", type=ConfigDataTypes.STRING, label="Control Network 1" ), Configuration( - _id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2" + id="controlnet2", type=ConfigDataTypes.STRING, label="Control Network 2" ), Configuration( - _id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3" + id="controlnet3", type=ConfigDataTypes.STRING, label="Control Network 3" ), Configuration( - _id="controlnet_updown_script", - _type=ConfigDataTypes.STRING, + id="controlnet_updown_script", + type=ConfigDataTypes.STRING, label="Control Network Script", ), Configuration( - _id="enablerj45", - _type=ConfigDataTypes.BOOL, + id="enablerj45", + type=ConfigDataTypes.BOOL, default="1", label="Enable RJ45s", ), Configuration( - _id="preservedir", - _type=ConfigDataTypes.BOOL, + id="preservedir", + type=ConfigDataTypes.BOOL, default="0", label="Preserve session dir", ), Configuration( - _id="enablesdt", - _type=ConfigDataTypes.BOOL, + id="enablesdt", + type=ConfigDataTypes.BOOL, default="0", label="Enable SDT3D output", ), Configuration( - _id="sdturl", - _type=ConfigDataTypes.STRING, + id="sdturl", + type=ConfigDataTypes.STRING, default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL", ), Configuration( - _id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS" + id="ovs", type=ConfigDataTypes.BOOL, default="0", label="Enable OVS" ), ] config_type: RegisterTlvs = RegisterTlvs.UTILITY diff --git a/daemon/core/errors.py b/daemon/core/errors.py index a75bd536..20ffc3a9 100644 --- a/daemon/core/errors.py +++ b/daemon/core/errors.py @@ -46,3 +46,11 @@ class CoreServiceBootError(Exception): """ pass + + +class CoreConfigError(Exception): + """ + Used when there is an error defining a configurable option. + """ + + pass diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 6acd6be7..8861d7f6 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -236,35 +236,35 @@ class BasicRangeModel(WirelessModel): name: str = "basic_range" options: List[Configuration] = [ Configuration( - _id="range", - _type=ConfigDataTypes.UINT32, + id="range", + type=ConfigDataTypes.UINT32, default="275", label="wireless range (pixels)", ), Configuration( - _id="bandwidth", - _type=ConfigDataTypes.UINT64, + id="bandwidth", + type=ConfigDataTypes.UINT64, default="54000000", label="bandwidth (bps)", ), Configuration( - _id="jitter", - _type=ConfigDataTypes.UINT64, + id="jitter", + type=ConfigDataTypes.UINT64, default="0", label="transmission jitter (usec)", ), Configuration( - _id="delay", - _type=ConfigDataTypes.UINT64, + id="delay", + type=ConfigDataTypes.UINT64, default="5000", label="transmission delay (usec)", ), Configuration( - _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" + id="error", type=ConfigDataTypes.STRING, default="0", label="loss (%)" ), Configuration( - _id="promiscuous", - _type=ConfigDataTypes.BOOL, + id="promiscuous", + type=ConfigDataTypes.BOOL, default="0", label="promiscuous mode", ), @@ -868,40 +868,38 @@ class Ns2ScriptedMobility(WayPointMobility): name: str = "ns2script" options: List[Configuration] = [ Configuration( - _id="file", _type=ConfigDataTypes.STRING, label="mobility script file" + id="file", type=ConfigDataTypes.STRING, label="mobility script file" ), Configuration( - _id="refresh_ms", - _type=ConfigDataTypes.UINT32, + id="refresh_ms", + type=ConfigDataTypes.UINT32, default="50", label="refresh time (ms)", ), + Configuration(id="loop", type=ConfigDataTypes.BOOL, default="1", label="loop"), Configuration( - _id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop" - ), - Configuration( - _id="autostart", - _type=ConfigDataTypes.STRING, + id="autostart", + type=ConfigDataTypes.STRING, label="auto-start seconds (0.0 for runtime)", ), Configuration( - _id="map", - _type=ConfigDataTypes.STRING, + id="map", + type=ConfigDataTypes.STRING, label="node mapping (optional, e.g. 0:1,1:2,2:3)", ), Configuration( - _id="script_start", - _type=ConfigDataTypes.STRING, + id="script_start", + type=ConfigDataTypes.STRING, label="script file to run upon start", ), Configuration( - _id="script_pause", - _type=ConfigDataTypes.STRING, + id="script_pause", + type=ConfigDataTypes.STRING, label="script file to run upon pause", ), Configuration( - _id="script_stop", - _type=ConfigDataTypes.STRING, + id="script_stop", + type=ConfigDataTypes.STRING, label="script file to run upon stop", ), ] diff --git a/daemon/examples/myemane/examplemodel.py b/daemon/examples/myemane/examplemodel.py index b9e6e148..c33ac166 100644 --- a/daemon/examples/myemane/examplemodel.py +++ b/daemon/examples/myemane/examplemodel.py @@ -1,6 +1,7 @@ """ Example custom emane model. """ +from pathlib import Path from typing import Dict, List, Optional, Set from core.config import Configuration @@ -39,17 +40,34 @@ class ExampleModel(emanemodel.EmaneModel): name: str = "emane_example" mac_library: str = "rfpipemaclayer" - mac_xml: str = "/usr/share/emane/manifest/rfpipemaclayer.xml" + mac_xml: str = "rfpipemaclayer.xml" mac_defaults: Dict[str, str] = { "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" } - mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + mac_config: List[Configuration] = [] phy_library: Optional[str] = None - phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" + phy_xml: str = "emanephy.xml" phy_defaults: Dict[str, str] = { "subid": "1", "propagationmodel": "2ray", "noisemode": "none", } - phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + phy_config: List[Configuration] = [] config_ignore: Set[str] = set() + + @classmethod + def load(cls, emane_prefix: Path) -> None: + """ + Called after being loaded within the EmaneManager. Provides configured + emane_prefix for parsing xml files. + + :param emane_prefix: configured emane prefix path + :return: nothing + """ + manifest_path = "share/emane/manifest" + # load mac configuration + mac_xml_path = emane_prefix / manifest_path / cls.mac_xml + cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults) + # load phy configuration + phy_xml_path = emane_prefix / manifest_path / cls.phy_xml + cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index e90acfbd..994a09f3 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -17,8 +17,8 @@ class TestConfigurableOptions(ConfigurableOptions): name1 = "value1" name2 = "value2" options = [ - Configuration(_id=name1, _type=ConfigDataTypes.STRING, label=name1), - Configuration(_id=name2, _type=ConfigDataTypes.STRING, label=name2), + Configuration(id=name1, type=ConfigDataTypes.STRING, label=name1), + Configuration(id=name2, type=ConfigDataTypes.STRING, label=name2), ] diff --git a/daemon/tests/test_config_services.py b/daemon/tests/test_config_services.py index 7efde087..432f2089 100644 --- a/daemon/tests/test_config_services.py +++ b/daemon/tests/test_config_services.py @@ -27,11 +27,11 @@ class MyService(ConfigService): shutdown = [f"pkill {files[0]}"] validation_mode = ConfigServiceMode.BLOCKING default_configs = [ - Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), - Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), + Configuration(id="value1", type=ConfigDataTypes.STRING, label="Text"), + Configuration(id="value2", type=ConfigDataTypes.BOOL, label="Boolean"), Configuration( - _id="value3", - _type=ConfigDataTypes.STRING, + id="value3", + type=ConfigDataTypes.STRING, label="Multiple Choice", options=["value1", "value2", "value3"], ), diff --git a/docs/emane.md b/docs/emane.md index f589f834..9e2e5b0b 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -120,7 +120,8 @@ Here is an example model with documentation describing functionality: """ Example custom emane model. """ -from typing import Dict, List, Optional, Set +from pathlib import Path +from typing import Dict, Optional, Set, List from core.config import Configuration from core.emane import emanemanifest, emanemodel @@ -162,14 +163,31 @@ class ExampleModel(emanemodel.EmaneModel): mac_defaults: Dict[str, str] = { "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" } - mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + mac_config: List[Configuration] = [] phy_library: Optional[str] = None phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" phy_defaults: Dict[str, str] = { "subid": "1", "propagationmodel": "2ray", "noisemode": "none" } - phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + phy_config: List[Configuration] = [] config_ignore: Set[str] = set() + + @classmethod + def load(cls, emane_prefix: Path) -> None: + """ + Called after being loaded within the EmaneManager. Provides configured + emane_prefix for parsing xml files. + + :param emane_prefix: configured emane prefix path + :return: nothing + """ + manifest_path = "share/emane/manifest" + # load mac configuration + mac_xml_path = emane_prefix / manifest_path / cls.mac_xml + cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults) + # load phy configuration + phy_xml_path = emane_prefix / manifest_path / cls.phy_xml + cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) ``` ## Single PC with EMANE