Writing a Custom Plugin¶
CORNET uses a plugin system for network and robot backends. If the built-in
ns3, mininet, and gazebo plugins don't cover your use case, you can
write a custom plugin.
1. Plugin interface¶
All plugins implement cornet.plugins.base.Plugin:
class Plugin(abc.ABC):
def configure(self, config: UnifiedConfig, context: ExperimentContext) -> None: ...
def start(self) -> None: ...
def run(self) -> None: ... # optional — default is a no-op
def stop(self) -> None: ...
def collect(self, output_dir: Path) -> None: ... # optional
Lifecycle order (called by the Orchestrator):
| Step | Method | Purpose |
|---|---|---|
| 1 | configure(config, context) |
Read config; prepare state. Must NOT start processes. |
| 2 | start() |
Launch subprocesses, create topology, etc. |
| 3 | run() |
Block for experiment duration (optional; Orchestrator handles timing). |
| 4 | stop() |
Tear down all processes. Called even on error — must be idempotent. |
| 5 | collect(output_dir) |
Copy metrics/logs to output_dir. |
2. ExperimentContext fields¶
The context object is shared across all plugins in a single run:
@dataclass
class ExperimentContext:
network: NetworkContext # context.network.node_ips: dict[str, str]
robot: RobotContext # context.robot.robot_namespaces: dict[str, str]
variant_id: str # "default" or "param=val+param=val" for sweeps
Network plugins populate context.network.node_ips during start().
Robot plugins read those IPs to configure ROS 2 namespaces or launch files.
3. Minimal custom network plugin skeleton¶
# cornet/plugins/network/my_plugin.py
from __future__ import annotations
from pathlib import Path
from cornet.config.schema import UnifiedConfig
from cornet.context import ExperimentContext
from cornet.plugins.base import Plugin
class MyNetworkPlugin(Plugin):
def configure(self, config: UnifiedConfig, context: ExperimentContext) -> None:
self._nodes = config.network.nodes
self._duration = config.experiment.duration
self._context = context
self._proc = None
def start(self) -> None:
# Start your network simulator
import subprocess
self._proc = subprocess.Popen(["my-simulator", "--nodes", str(len(self._nodes))])
# Populate node IPs so robot plugin can use them
for i, node in enumerate(self._nodes):
self._context.network.node_ips[node.name] = f"10.0.{i}.1"
def stop(self) -> None:
if self._proc is not None:
self._proc.terminate()
self._proc.wait(timeout=10)
self._proc = None
def collect(self, output_dir: Path) -> None:
# Copy your simulator's output to output_dir
import shutil
sim_output = Path("/tmp/my-simulator-output")
if sim_output.exists():
shutil.copytree(sim_output, output_dir / "network", dirs_exist_ok=True)
4. Register the plugin¶
Add an entry to the plugin registry in cornet/plugins/__init__.py (or wherever your project resolves plugin names):
NETWORK_PLUGINS = {
"ns3": "cornet.plugins.network.ns3_plugin.NS3Plugin",
"mininet": "cornet.plugins.network.mininet_plugin.MininetPlugin",
"my-network": "cornet.plugins.network.my_plugin.MyNetworkPlugin", # add this
}
Then use plugin: my-network in config.yaml:
network:
plugin: my-network
type: ns3 # keep a valid type for schema validation; your plugin ignores it
5. Testing your plugin¶
Write unit tests that call the lifecycle methods directly:
def test_my_plugin_lifecycle(tmp_path):
from cornet.config.schema import UnifiedConfig
from cornet.context import ExperimentContext
from cornet.plugins.network.my_plugin import MyNetworkPlugin
config = UnifiedConfig.model_validate({...})
context = ExperimentContext()
plugin = MyNetworkPlugin()
plugin.configure(config, context)
plugin.start()
plugin.stop()
plugin.collect(tmp_path)
assert (tmp_path / "network").exists()