diff --git a/config.yaml.example b/config.yaml.example index a594677..c4b0546 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -34,6 +34,13 @@ repeater: # Recommended: 10 hours for typical deployments send_advert_interval_hours: 10 +# Mesh Network Configuration +mesh: + # Global flood policy - controls whether the repeater allows or denies flooding by default + # true = allow flooding globally, false = deny flooding globally + # Individual transport keys can override this setting + global_flood_allow: false + radio: # Frequency in Hz (869.618 MHz for EU) frequency: 869618000 diff --git a/repeater/config.py b/repeater/config.py index ffad780..4716bf5 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -45,6 +45,70 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: return config +def save_config(config_data: Dict[str, Any], config_path: Optional[str] = None) -> bool: + """ + Save configuration to YAML file. + + Args: + config_data: Configuration dictionary to save + config_path: Path to config file (uses default if None) + + Returns: + True if successful, False otherwise + """ + if config_path is None: + config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/pymc_repeater/config.yaml") + + try: + # Create backup of existing config + config_file = Path(config_path) + if config_file.exists(): + backup_path = config_file.with_suffix('.yaml.backup') + config_file.rename(backup_path) + logger.info(f"Created backup at {backup_path}") + + # Save new config + with open(config_path, 'w') as f: + yaml.safe_dump(config_data, f, default_flow_style=False, sort_keys=False) + + logger.info(f"Saved configuration to {config_path}") + return True + + except Exception as e: + logger.error(f"Failed to save configuration: {e}") + return False + + +def update_global_flood_policy(allow: bool, config_path: Optional[str] = None) -> bool: + """ + Update the global flood policy in the configuration. + + Args: + allow: True to allow flooding globally, False to deny + config_path: Path to config file (uses default if None) + + Returns: + True if successful, False otherwise + """ + try: + # Load current config + config = load_config(config_path) + + # Ensure mesh section exists + if "mesh" not in config: + config["mesh"] = {} + + # Set global flood policy + config["mesh"]["global_flood_allow"] = allow + + # Save updated config + return save_config(config, config_path) + + except Exception as e: + logger.error(f"Failed to update global flood policy: {e}") + return False + + def _load_or_create_identity_key(path: Optional[str] = None) -> bytes: if path is None: diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 0e3e91b..cf60534 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import Callable, Optional import cherrypy from repeater import __version__ +from repeater.config import update_global_flood_policy from .cad_calibration_engine import CADCalibrationEngine logger = logging.getLogger("HTTPServer") @@ -804,4 +805,42 @@ class APIEndpoints: return self._error("Invalid key_id format") except Exception as e: logger.error(f"Error deleting transport key: {e}") - return self._error(e) \ No newline at end of file + return self._error(e) + + @cherrypy.expose + @cors_enabled + @cherrypy.tools.json_in() + def global_flood_policy(self): + """ + Update global flood policy configuration + + POST /global_flood_policy + Body: {"global_flood_allow": true/false} + """ + if cherrypy.request.method == "POST": + try: + data = cherrypy.request.json or {} + global_flood_allow = data.get("global_flood_allow") + + if global_flood_allow is None: + return self._error("Missing required field: global_flood_allow") + + if not isinstance(global_flood_allow, bool): + return self._error("global_flood_allow must be a boolean value") + + # Update the configuration + success = update_global_flood_policy(global_flood_allow) + + if success: + return self._success( + {"global_flood_allow": global_flood_allow}, + message=f"Global flood policy updated to {'allow' if global_flood_allow else 'deny'}" + ) + else: + return self._error("Failed to update global flood policy configuration") + + except Exception as e: + logger.error(f"Error updating global flood policy: {e}") + return self._error(e) + else: + return self._error("Method not supported") \ No newline at end of file