From 0311c54fdaa4267d2c3a298fe7c78c95d7e5ffa1 Mon Sep 17 00:00:00 2001 From: Mary Salvi Date: Fri, 11 Sep 2026 12:39:42 -0400 Subject: [PATCH 1/4] Add management command for FFW --- .../core/management/commands/run_FFW.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 net_maestro/core/management/commands/run_FFW.py diff --git a/net_maestro/core/management/commands/run_FFW.py b/net_maestro/core/management/commands/run_FFW.py new file mode 100644 index 00000000..05119651 --- /dev/null +++ b/net_maestro/core/management/commands/run_FFW.py @@ -0,0 +1,84 @@ +""" RUN FFW simulation management command. +Execute Fluid-Flow WAN simulation with specified parameters and ingest results. +""" +from __future__ import annotations + +from pathlib import Path +import subprocess + +import djclick as click + + + +def _build_ffw_command( + np: int, + binary_path: Path, + sync: int, + config_path: Path + ) -> list[str]: + """Build the FFW command to run the simulation.""" + return [ + "mpirun", + "-np", + str(np), + str(binary_path), + f"--sync={sync}", + "--", + str(config_path), + ] + +@click.command() +@click.option( + "--np", + "np", + type=int, + default=1, + help="Number of MPI processes.", +) +@click.option( + "--binary-path", + "binary_path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + help="Path to FFW binary.", + default="/opt/FFW/build/local/src/model-net-fluid-flow-wan-random-traffic" +) +@click.option( + "--sync", + "sync", + type=int, + default=1, + help=( + "Synchronization protocol (1=sequential, 2=conservative, 3=optimistic, " + "4=optimistic-debug, 5=optimistic-realtime, 6=reverse-check)." + ), +) +@click.option( + "--config-path", + "config_path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + help="Path to FFW configuration file.", + default="/opt/FFW/build/local/doc/example/fluid-flow-wan-random-traffic.yaml" +) +@click.option( + "--working-dir", + default="/opt/FFW/build/local", + type=click.Path(exists=True, path_type=Path), + ) + +def run_FFW( + np: int, + binary_path: Path, + sync: int, + config_path: Path, + working_dir: Path +)-> None: + + working_dir = Path(working_dir) + # Logs dir needs to exist for the model to run + logs_dir = working_dir/ "doc" / "example" / "logs" + logs_dir.mkdir(parents=True, exist_ok=True) + + cmd = _build_ffw_command(np=np, binary_path=binary_path, sync=sync, config_path=config_path) + click.echo(f"Running FFW: {' '.join(map(str, cmd))}") + + subprocess.run(cmd, cwd=working_dir, check=True) From 86b7b51a4a17f89ce6006c732ea6216fb73b16c8 Mon Sep 17 00:00:00 2001 From: Mary Salvi Date: Fri, 11 Sep 2026 13:22:56 -0400 Subject: [PATCH 2/4] Add Run entry when executing ffw --- dev/.env.docker-compose | 5 +- .../core/management/commands/run_FFW.py | 92 +++++++++++++++++-- net_maestro/settings/development.py | 4 + 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/dev/.env.docker-compose b/dev/.env.docker-compose index 3afedea1..34e8b339 100644 --- a/dev/.env.docker-compose +++ b/dev/.env.docker-compose @@ -23,4 +23,7 @@ DJANGO_PHOLD_MPI_PROCESSES=4 CODE_EXAMPLE_DIR=/opt/codes # Directory containing FFW build -FFW_BUILD_DIR=/opt/FFW +DJANGO_FFW_BUILD_DIR=/opt/FFW/build/local + +# Directory for FFW output files +DJANGO_FFW_OUTPUT_DIR=/tmp/ffw_output diff --git a/net_maestro/core/management/commands/run_FFW.py b/net_maestro/core/management/commands/run_FFW.py index 05119651..5c2e8264 100644 --- a/net_maestro/core/management/commands/run_FFW.py +++ b/net_maestro/core/management/commands/run_FFW.py @@ -8,6 +8,10 @@ import djclick as click +from net_maestro import settings +from net_maestro.core.constants import RunStatus, SimulationType +from net_maestro.core.models import Run + def _build_ffw_command( @@ -27,7 +31,48 @@ def _build_ffw_command( str(config_path), ] +def _create_or_update_run( + name: str, + description: str | None, + run_id: int | None, +) -> Run: + """Create a new Run object or update an existing one's metadata. + + Args: + name: Run identifier + description: Run description + run_id: Existing run ID to update, or None to create new + + Returns: + The Run object + + Raises: + Run.DoesNotExist: If run_id is provided but no matching Run exists + """ + if run_id: + run = Run.objects.get(id=run_id) + run.name = name + run.description = description or "" + run.save() + else: + run = Run.objects.create( + name=name, + description=description or "", + status=RunStatus.RUNNING, + simulation_type=SimulationType.FFW, + ) + return run + + @click.command() +@click.option( + "-n", + "--name", + "name", + type=str, + help="Run identifier.", + required=True, +) @click.option( "--np", "np", @@ -40,7 +85,7 @@ def _build_ffw_command( "binary_path", type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Path to FFW binary.", - default="/opt/FFW/build/local/src/model-net-fluid-flow-wan-random-traffic" + required=True, ) @click.option( "--sync", @@ -57,23 +102,37 @@ def _build_ffw_command( "config_path", type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Path to FFW configuration file.", - default="/opt/FFW/build/local/doc/example/fluid-flow-wan-random-traffic.yaml" + required=True, ) @click.option( "--working-dir", - default="/opt/FFW/build/local", type=click.Path(exists=True, path_type=Path), ) - +@click.option( + "--description", + "description", + type=str, + help="Description of the run.", +) +@click.option( + "--run-id", + "run_id", + type=int, + default=None, + help="Existing run ID to update instead of creating a new run.", +) def run_FFW( + name: str, np: int, binary_path: Path, sync: int, config_path: Path, - working_dir: Path + working_dir: Path, + description: str | None = None, + run_id: int | None = None, )-> None: - working_dir = Path(working_dir) + working_dir = Path(working_dir) if working_dir else Path(getattr(settings, "FFW_BINARY_DIR", ".")) # Logs dir needs to exist for the model to run logs_dir = working_dir/ "doc" / "example" / "logs" logs_dir.mkdir(parents=True, exist_ok=True) @@ -81,4 +140,23 @@ def run_FFW( cmd = _build_ffw_command(np=np, binary_path=binary_path, sync=sync, config_path=config_path) click.echo(f"Running FFW: {' '.join(map(str, cmd))}") - subprocess.run(cmd, cwd=working_dir, check=True) + # Create or update Run object + run = _create_or_update_run(name, description, run_id) + + try: + result = subprocess.run(cmd, cwd=working_dir, check=True, capture_output=True, text=True) + click.echo(f"STDOUT:\n {result.stdout}") + run.status = RunStatus.COMPLETED + run.save() + if result.stderr: + click.echo(f"STDERR:\n {result.stderr}") + click.echo("FFW simulation finished successfully.") + click.echo(f"Logs are in {logs_dir}") + click.echo(f"\nRun {run.id} created successfully.") + except subprocess.CalledProcessError as e: + click.echo(f"FFW simulation failed with error code {e.returncode}.") + click.echo(f"STDOUT:\n {e.stdout}") + click.echo(f"STDERR:\n {e.stderr}") + run.status = RunStatus.FAILED + run.save() + raise diff --git a/net_maestro/settings/development.py b/net_maestro/settings/development.py index c57ca9aa..bb07b208 100644 --- a/net_maestro/settings/development.py +++ b/net_maestro/settings/development.py @@ -65,3 +65,7 @@ PHOLD_BINARY_PATH = env.str("DJANGO_PHOLD_BINARY_PATH", default="") PHOLD_OUTPUT_DIR = env.str("DJANGO_PHOLD_OUTPUT_DIR", default="/tmp/phold_output") # noqa: S108 PHOLD_MPI_PROCESSES = env.int("DJANGO_PHOLD_MPI_PROCESSES", default=2) + +# FFW simulation configuration +FFW_BINARY_PATH = env.str("DJANGO_FFW_BINARY_PATH" / "src" / "model-net-fluid-flow-wan-random-traffic", default="") +FFW_OUTPUT_DIR = env.str("DJANGO_FFW_OUTPUT_DIR", default="/tmp/ffw_output") # noqa: S108 From 12d3b7ee72b926b802f0d30071834d6ba695dbae Mon Sep 17 00:00:00 2001 From: Mary Salvi Date: Wed, 16 Sep 2026 14:42:12 -0400 Subject: [PATCH 3/4] Add celery task and refactor model execution to service --- dev/.env.docker-compose | 6 ++ .../commands/{run_FFW.py => run_ffw.py} | 73 ++++++------------- net_maestro/core/services/__init__.py | 0 net_maestro/core/services/ffw.py | 32 ++++++++ net_maestro/core/tasks/__init__.py | 10 ++- net_maestro/core/tasks/simulation.py | 39 ++++++++++ net_maestro/settings/development.py | 4 +- 7 files changed, 111 insertions(+), 53 deletions(-) rename net_maestro/core/management/commands/{run_FFW.py => run_ffw.py} (61%) create mode 100644 net_maestro/core/services/__init__.py create mode 100644 net_maestro/core/services/ffw.py diff --git a/dev/.env.docker-compose b/dev/.env.docker-compose index 34e8b339..78b32a58 100644 --- a/dev/.env.docker-compose +++ b/dev/.env.docker-compose @@ -25,5 +25,11 @@ CODE_EXAMPLE_DIR=/opt/codes # Directory containing FFW build DJANGO_FFW_BUILD_DIR=/opt/FFW/build/local +# FFW binary +DJANGO_FFW_BINARY_PATH=/opt/FFW/build/local/src/model-net-fluid-flow-wan-random-traffic + +# FFW Config path +DJANGO_FFW_CONFIG_PATH=/opt/FFW/build/local/doc/example/fluid-flow-wan-random-traffic.yaml + # Directory for FFW output files DJANGO_FFW_OUTPUT_DIR=/tmp/ffw_output diff --git a/net_maestro/core/management/commands/run_FFW.py b/net_maestro/core/management/commands/run_ffw.py similarity index 61% rename from net_maestro/core/management/commands/run_FFW.py rename to net_maestro/core/management/commands/run_ffw.py index 5c2e8264..2c8b55d4 100644 --- a/net_maestro/core/management/commands/run_FFW.py +++ b/net_maestro/core/management/commands/run_ffw.py @@ -1,36 +1,20 @@ -""" RUN FFW simulation management command. +"""RUN FFW simulation management command. + Execute Fluid-Flow WAN simulation with specified parameters and ingest results. """ + from __future__ import annotations from pathlib import Path -import subprocess +from django.conf import settings import djclick as click -from net_maestro import settings from net_maestro.core.constants import RunStatus, SimulationType from net_maestro.core.models import Run +from net_maestro.core.tasks.simulation import run_ffw_simulation - -def _build_ffw_command( - np: int, - binary_path: Path, - sync: int, - config_path: Path - ) -> list[str]: - """Build the FFW command to run the simulation.""" - return [ - "mpirun", - "-np", - str(np), - str(binary_path), - f"--sync={sync}", - "--", - str(config_path), - ] - def _create_or_update_run( name: str, description: str | None, @@ -58,7 +42,7 @@ def _create_or_update_run( run = Run.objects.create( name=name, description=description or "", - status=RunStatus.RUNNING, + status=RunStatus.PENDING, simulation_type=SimulationType.FFW, ) return run @@ -85,7 +69,6 @@ def _create_or_update_run( "binary_path", type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Path to FFW binary.", - required=True, ) @click.option( "--sync", @@ -102,12 +85,11 @@ def _create_or_update_run( "config_path", type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Path to FFW configuration file.", - required=True, ) @click.option( "--working-dir", type=click.Path(exists=True, path_type=Path), - ) +) @click.option( "--description", "description", @@ -121,7 +103,7 @@ def _create_or_update_run( default=None, help="Existing run ID to update instead of creating a new run.", ) -def run_FFW( +def run_ffw( # noqa: PLR0913 name: str, np: int, binary_path: Path, @@ -130,33 +112,24 @@ def run_FFW( working_dir: Path, description: str | None = None, run_id: int | None = None, -)-> None: +) -> None: - working_dir = Path(working_dir) if working_dir else Path(getattr(settings, "FFW_BINARY_DIR", ".")) + working_dir = ( + Path(working_dir) if working_dir else Path(getattr(settings, "FFW_BUILD_PATH", ".")) + ) # Logs dir needs to exist for the model to run - logs_dir = working_dir/ "doc" / "example" / "logs" + logs_dir = working_dir / "doc" / "example" / "logs" logs_dir.mkdir(parents=True, exist_ok=True) - cmd = _build_ffw_command(np=np, binary_path=binary_path, sync=sync, config_path=config_path) - click.echo(f"Running FFW: {' '.join(map(str, cmd))}") - # Create or update Run object run = _create_or_update_run(name, description, run_id) - - try: - result = subprocess.run(cmd, cwd=working_dir, check=True, capture_output=True, text=True) - click.echo(f"STDOUT:\n {result.stdout}") - run.status = RunStatus.COMPLETED - run.save() - if result.stderr: - click.echo(f"STDERR:\n {result.stderr}") - click.echo("FFW simulation finished successfully.") - click.echo(f"Logs are in {logs_dir}") - click.echo(f"\nRun {run.id} created successfully.") - except subprocess.CalledProcessError as e: - click.echo(f"FFW simulation failed with error code {e.returncode}.") - click.echo(f"STDOUT:\n {e.stdout}") - click.echo(f"STDERR:\n {e.stderr}") - run.status = RunStatus.FAILED - run.save() - raise + run_ffw_simulation.apply( + kwargs={ + "run_id": run.id, + "np": np, + "sync": sync, + "config_path": str(config_path) if config_path else None, + "working_dir": str(working_dir), + "binary_path": str(binary_path) if binary_path else None, + } + ) diff --git a/net_maestro/core/services/__init__.py b/net_maestro/core/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/net_maestro/core/services/ffw.py b/net_maestro/core/services/ffw.py new file mode 100644 index 00000000..2232187b --- /dev/null +++ b/net_maestro/core/services/ffw.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import logging +import subprocess + +logger = logging.getLogger(__name__) + + +def execute_ffw_model( + *, + np: int, + sync: int, + config_path: str, + working_dir: str, + binary_path: str, +) -> int: + cmd = [ + "mpirun", + "-np", + str(np), + binary_path, + f"--sync={sync}", + "--", + config_path, + ] + try: + result = subprocess.run(cmd, cwd=working_dir, check=True, capture_output=True, text=True) # noqa: S603 + logger.info("FFW simulation stdout: %s", result.stdout) + except subprocess.CalledProcessError as e: + logger.exception("FFW simulation failed with error code %s", e.returncode) + logger.info("FFW simulation stderr: %s", result.stderr) + return result.returncode diff --git a/net_maestro/core/tasks/__init__.py b/net_maestro/core/tasks/__init__.py index 72a53e30..635d26da 100644 --- a/net_maestro/core/tasks/__init__.py +++ b/net_maestro/core/tasks/__init__.py @@ -2,6 +2,12 @@ from .events import run_event_task from .models import run_model_task -from .simulation import run_phold_simulation, run_simulation_task +from .simulation import run_ffw_simulation, run_phold_simulation, run_simulation_task -__all__ = ["run_event_task", "run_model_task", "run_phold_simulation", "run_simulation_task"] +__all__ = [ + "run_event_task", + "run_ffw_simulation", + "run_model_task", + "run_phold_simulation", + "run_simulation_task", +] diff --git a/net_maestro/core/tasks/simulation.py b/net_maestro/core/tasks/simulation.py index ae7d64fd..4dbdd81b 100644 --- a/net_maestro/core/tasks/simulation.py +++ b/net_maestro/core/tasks/simulation.py @@ -18,6 +18,7 @@ from net_maestro.core.models.simulation_pe_record import SimulationPeRecord from net_maestro.core.parsers.ross_binary_file import RecordType from net_maestro.core.parsers.ross_binary_file import ROSSFile as SimulationFileParser +from net_maestro.core.services.ffw import execute_ffw_model logger = logging.getLogger(__name__) @@ -219,3 +220,41 @@ def run_phold_simulation( # noqa: PLR0913 # Update status to FAILED run.status = RunStatus.FAILED run.save() + + +@shared_task +def run_ffw_simulation( # noqa: PLR0913 + *, + run_id: int, + np: int = 1, + sync: int = 1, + working_dir: str = "", + binary_path: str = "", + config_path: str = "", +) -> None: + """Execute FFW simulation with given parameters and update run status.""" + run = Run.objects.get(pk=run_id) + run.status = RunStatus.RUNNING + run.save() + + # Use example defaults if values not provided + ffw_working_dir = working_dir or getattr(settings, "FFW_BUILD_PATH", "") + ffw_binary_path = binary_path or getattr(settings, "FFW_BINARY_PATH", "") + ffw_config_path = config_path or getattr(settings, "FFW_CONFIG_PATH", "") + + try: + execute_ffw_model( + np=np, + sync=sync, + config_path=ffw_config_path, + working_dir=ffw_working_dir, + binary_path=ffw_binary_path, + ) + run.status = RunStatus.COMPLETED + run.save() + logger.info("FFW simulation finished for run %s", run_id) + + except Exception: + run.status = RunStatus.FAILED + run.save() + logger.exception("FFW simulation failed for run %s", run_id) diff --git a/net_maestro/settings/development.py b/net_maestro/settings/development.py index bb07b208..2a1f3f3b 100644 --- a/net_maestro/settings/development.py +++ b/net_maestro/settings/development.py @@ -67,5 +67,7 @@ PHOLD_MPI_PROCESSES = env.int("DJANGO_PHOLD_MPI_PROCESSES", default=2) # FFW simulation configuration -FFW_BINARY_PATH = env.str("DJANGO_FFW_BINARY_PATH" / "src" / "model-net-fluid-flow-wan-random-traffic", default="") +FFW_BUILD_PATH = env.str("DJANGO_FFW_BUILD_DIR", default="") +FFW_BINARY_PATH = env.str("DJANGO_FFW_BINARY_PATH", default="") +FFW_CONFIG_PATH = env.str("DJANGO_FFW_CONFIG_PATH", default="") FFW_OUTPUT_DIR = env.str("DJANGO_FFW_OUTPUT_DIR", default="/tmp/ffw_output") # noqa: S108 From eed6d6ddc704bec4801c75be7b52d4e3e89c1c48 Mon Sep 17 00:00:00 2001 From: Mary Salvi Date: Thu, 24 Sep 2026 14:01:00 -0400 Subject: [PATCH 4/4] Add command options that produce expected ross binary files --- dev/.env.docker-compose | 4 +-- .../core/management/commands/run_ffw.py | 33 ++++++++++++++++++- net_maestro/core/services/ffw.py | 16 ++++++++- net_maestro/core/tasks/simulation.py | 10 ++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/dev/.env.docker-compose b/dev/.env.docker-compose index 78b32a58..eb188bad 100644 --- a/dev/.env.docker-compose +++ b/dev/.env.docker-compose @@ -26,10 +26,10 @@ CODE_EXAMPLE_DIR=/opt/codes DJANGO_FFW_BUILD_DIR=/opt/FFW/build/local # FFW binary -DJANGO_FFW_BINARY_PATH=/opt/FFW/build/local/src/model-net-fluid-flow-wan-random-traffic +DJANGO_FFW_BINARY_PATH=/opt/FFW/build/local/src/model-net-fluid-flow-wan-trace-traffic # FFW Config path -DJANGO_FFW_CONFIG_PATH=/opt/FFW/build/local/doc/example/fluid-flow-wan-random-traffic.yaml +DJANGO_FFW_CONFIG_PATH=/opt/FFW/build/local/doc/example/fluid-flow-wan-trace-traffic-pause.yaml # Directory for FFW output files DJANGO_FFW_OUTPUT_DIR=/tmp/ffw_output diff --git a/net_maestro/core/management/commands/run_ffw.py b/net_maestro/core/management/commands/run_ffw.py index 2c8b55d4..b0536098 100644 --- a/net_maestro/core/management/commands/run_ffw.py +++ b/net_maestro/core/management/commands/run_ffw.py @@ -48,6 +48,8 @@ def _create_or_update_run( return run +# TODO: consider adding the stats path as an option +# get help text for new options @click.command() @click.option( "-n", @@ -80,6 +82,26 @@ def _create_or_update_run( "4=optimistic-debug, 5=optimistic-realtime, 6=reverse-check)." ), ) +@click.option( + "--model-stats", + "model_stats", + type=int, + default=4, +) +@click.option( + "--num-gvt", + "num_gvt", + type=int, + default=1, +) +@click.option( + "--rt-interval", + "rt_interval", + type=int, + default=1, +) +@click.option("--vt-interval", "vt_interval", type=int, default=1e8) +@click.option("--vt-samp-end", "vt_samp_end", type=int, default=1.7e10) @click.option( "--config-path", "config_path", @@ -108,12 +130,16 @@ def run_ffw( # noqa: PLR0913 np: int, binary_path: Path, sync: int, + model_stats: int, + num_gvt: int, + rt_interval: int, + vt_interval: int, + vt_samp_end: int, config_path: Path, working_dir: Path, description: str | None = None, run_id: int | None = None, ) -> None: - working_dir = ( Path(working_dir) if working_dir else Path(getattr(settings, "FFW_BUILD_PATH", ".")) ) @@ -128,6 +154,11 @@ def run_ffw( # noqa: PLR0913 "run_id": run.id, "np": np, "sync": sync, + "model_stats": model_stats, + "num_gvt": num_gvt, + "rt_interval": rt_interval, + "vt_interval": vt_interval, + "vt_samp_end": vt_samp_end, "config_path": str(config_path) if config_path else None, "working_dir": str(working_dir), "binary_path": str(binary_path) if binary_path else None, diff --git a/net_maestro/core/services/ffw.py b/net_maestro/core/services/ffw.py index 2232187b..dc3d26ff 100644 --- a/net_maestro/core/services/ffw.py +++ b/net_maestro/core/services/ffw.py @@ -3,23 +3,37 @@ import logging import subprocess +from django.conf import settings + logger = logging.getLogger(__name__) -def execute_ffw_model( +def execute_ffw_model( # noqa: PLR0913 *, np: int, sync: int, + model_stats: int, + num_gvt: int, + rt_interval: int, + vt_interval: int, + vt_samp_end: int, config_path: str, working_dir: str, binary_path: str, ) -> int: + stats_output = settings.FFW_OUTPUT_DIR cmd = [ "mpirun", "-np", str(np), binary_path, f"--sync={sync}", + f"--model-stats={model_stats}", + f"--num-gvt={num_gvt}", + f"--rt-interval={rt_interval}", + f"--vt-interval={vt_interval}", + f"--vt-samp-end={vt_samp_end}", + f"--stats-path={stats_output}", "--", config_path, ] diff --git a/net_maestro/core/tasks/simulation.py b/net_maestro/core/tasks/simulation.py index 4dbdd81b..bc43ffe3 100644 --- a/net_maestro/core/tasks/simulation.py +++ b/net_maestro/core/tasks/simulation.py @@ -228,6 +228,11 @@ def run_ffw_simulation( # noqa: PLR0913 run_id: int, np: int = 1, sync: int = 1, + model_stats: int = 3, + num_gvt: int = 1, + rt_interval: int = 1, + vt_interval: int = 1e8, + vt_samp_end: int = 1.7e10, working_dir: str = "", binary_path: str = "", config_path: str = "", @@ -246,6 +251,11 @@ def run_ffw_simulation( # noqa: PLR0913 execute_ffw_model( np=np, sync=sync, + model_stats=model_stats, + num_gvt=num_gvt, + rt_interval=rt_interval, + vt_interval=vt_interval, + vt_samp_end=vt_samp_end, config_path=ffw_config_path, working_dir=ffw_working_dir, binary_path=ffw_binary_path,