""" Action Loader — dynamically discovers and loads action modules from the actions/ directory. Each .py file is a self-contained action. """ import importlib.util import logging import os import sys logger = logging.getLogger("sos.actions") class ActionRegistry: """Holds all registered actions by name.""" def __init__(self): self._actions = {} def register(self, action): """Register an action instance.""" self._actions[action.name] = action def get(self, name: str): """Get an action by name, or None.""" return self._actions.get(name) def list(self): """Return all registered action names.""" return list(self._actions.keys()) def run(self, name: str, ctx: dict, params: dict): """Run an action by name. Raises KeyError if not found.""" action = self.get(name) if action is None: raise KeyError(f"Unknown action: '{name}' (available: {self.list()})") return action.run(ctx, params) def load_actions(actions_dir: str) -> ActionRegistry: """ Scan actions_dir for .py files (except __init__.py) and load each. Each module must export a class named 'Action' with: - name: str (the playbook-visible name) - def run(self, ctx: dict, params: dict) -> any Returns an ActionRegistry with all loaded actions. """ registry = ActionRegistry() if not os.path.isdir(actions_dir): logger.warning("Actions directory not found: %s", actions_dir) return registry sys.path.insert(0, os.path.dirname(actions_dir)) for fname in sorted(os.listdir(actions_dir)): if not fname.endswith(".py") or fname == "__init__.py": continue mod_path = os.path.join(actions_dir, fname) mod_name = f"sos_actions_{fname[:-3]}" spec = importlib.util.spec_from_file_location(mod_name, mod_path) if spec is None or spec.loader is None: logger.warning("Cannot load action module: %s", fname) continue try: mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) action_class = getattr(mod, "Action", None) if action_class is None: logger.warning("Action %s has no 'Action' class — skipping", fname) continue instance = action_class() registry.register(instance) logger.info("Loaded action: %s (%s)", instance.name, fname) except Exception as e: logger.warning("Failed to load action %s: %s", fname, e) sys.path.pop(0) return registry