Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ cd NetEngine
# 2. Install dependencies
poetry install

# 3. Verify host prerequisites
poetry run netengine doctor
# 3. Verify host prerequisites and requested world subnets
poetry run netengine doctor --spec examples/minimal.yaml

# 4. Start local Postgres + pgmq (includes pgmq extension pre-installed)
docker compose up -d postgres
Expand All @@ -63,7 +63,7 @@ poetry run python -m netengine.utils.run_migrations
poetry run netengine up examples/minimal.yaml
```

If you only want host/container checks before configuring Postgres, run `poetry run netengine doctor --skip-db`. Check status at any time:
If you only want host/container checks before configuring Postgres, run `poetry run netengine doctor --skip-db`. Add `--spec examples/minimal.yaml` (or pass the spec as a positional argument) to include subnet-overlap checks for `spec.substrate.networks[*].subnet` before bootstrapping. Check status at any time:

```bash
poetry run netengine status
Expand Down
6 changes: 3 additions & 3 deletions docs/alpha-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ This guide is the focused alpha operator path. The root `README.md` keeps the hi
git clone https://github.com/Forebase/NetEngine.git
cd NetEngine
poetry install
poetry run netengine doctor --skip-db
poetry run netengine doctor --skip-db --spec examples/minimal.yaml
docker compose up -d postgres
poetry run python -m netengine.utils.run_migrations
poetry run netengine doctor
poetry run netengine doctor --spec examples/minimal.yaml
poetry run netengine up examples/minimal.yaml
poetry run netengine status
```

Use `NETENGINE_MOCK=true` when you want to exercise orchestration and spec validation without creating Docker, DNS, PKI, or identity resources.
Use `poetry run netengine doctor --spec examples/minimal.yaml` before bootstrapping a world to check host prerequisites plus Docker subnet conflicts against `spec.substrate.networks[*].subnet`. Use `NETENGINE_MOCK=true` when you want to exercise orchestration and spec validation without creating Docker, DNS, PKI, or identity resources.

## Pre-release mock smoke test

Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Start with the commands below before changing state:

```bash
poetry run netengine doctor
poetry run netengine doctor --spec examples/minimal.yaml
poetry run netengine status
poetry run netengine diagnose <spec.yaml>
poetry run netengine events
Expand All @@ -22,7 +22,7 @@ Do not manually mark phases complete. Fix the underlying dependency and re-run `

## Docker unavailable or stale resources

If Docker is expected to be available, start Docker Desktop or the Docker daemon and rerun `doctor`. If you intentionally want no infrastructure side effects, set mock mode:
If Docker is expected to be available, start Docker Desktop or the Docker daemon and rerun `doctor`. To catch Docker subnet conflicts before `up`, run `poetry run netengine doctor --spec <spec.yaml>`; if it reports that an existing Docker network reuses or overlaps a requested world subnet, remove the stale network with `docker network rm <name>` or choose a different subnet in the spec. If you intentionally want no infrastructure side effects, set mock mode:

```bash
NETENGINE_MOCK=true poetry run netengine up examples/minimal.yaml
Expand Down
65 changes: 60 additions & 5 deletions netengine/cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
build_context,
run_preflight,
)
from netengine.spec.loader import SpecLoadError, load_spec

__all__ = [
"DoctorCheckResult",
Expand Down Expand Up @@ -55,13 +56,29 @@


def run_checks(
db_url: str | None, state_file: Path, *, skip_db: bool = False
db_url: str | None,
state_file: Path,
*,
skip_db: bool = False,
spec_subnets: tuple[str, ...] = (),
) -> list[DoctorCheckResult]:
"""Run host-readiness checks without requiring a loaded NetEngine spec."""
# Keep monkeypatches of the legacy CLI module effective for callers/tests.
_preflight._run = _run
_preflight._can_bind = _can_bind
return _preflight.run_checks(db_url, state_file, skip_db=skip_db)
return _preflight.run_checks(
db_url, state_file, skip_db=skip_db, spec_subnets=spec_subnets
)


def _spec_subnets_from_file(spec_path: Path) -> tuple[str, ...]:
"""Load a world spec and return substrate network subnets for doctor checks."""
spec = load_spec(str(spec_path))
return tuple(
str(network.subnet)
for network in spec.substrate.networks.values()
if getattr(network, "subnet", None)
)


def _print_report(results: Iterable[DoctorCheckResult]) -> None:
Expand All @@ -81,10 +98,48 @@ def _print_report(results: Iterable[DoctorCheckResult]) -> None:
help="Runtime state file path.",
)
@click.option("--json", "as_json", is_flag=True, help="Output machine-readable JSON.")
@click.option("--skip-db", is_flag=True, help="Skip database connectivity and pgmq checks.")
def doctor(db_url: str | None, state_file: Path, as_json: bool, skip_db: bool) -> None:
@click.option(
"--skip-db", is_flag=True, help="Skip database connectivity and pgmq checks."
)
@click.option(
"--spec",
"spec_option",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="World spec whose substrate subnets should be checked for Docker conflicts.",
)
@click.argument(
"spec_arg",
required=False,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
)
def doctor(
db_url: str | None,
state_file: Path,
as_json: bool,
skip_db: bool,
spec_option: Path | None,
spec_arg: Path | None,
) -> None:
"""Run local host preflight checks before booting; use diagnose for world health."""
results = run_checks(db_url, state_file, skip_db=skip_db)
if spec_option and spec_arg:
raise click.UsageError(
"provide a spec path either as --spec or as the positional argument, not both"
)

spec_path = spec_option or spec_arg
spec_subnets: tuple[str, ...] = ()
if spec_path is not None:
try:
spec_subnets = _spec_subnets_from_file(spec_path)
except SpecLoadError as exc:
raise click.ClickException(f"spec validation failed: {exc}") from exc

if spec_path is None:
results = run_checks(db_url, state_file, skip_db=skip_db)
else:
results = run_checks(
db_url, state_file, skip_db=skip_db, spec_subnets=spec_subnets
)
if as_json:
click.echo(json.dumps([asdict(r) for r in results], indent=2))
else:
Expand Down
29 changes: 24 additions & 5 deletions netengine/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def _load_spec_for_cli(
):
"""Load a spec using the same composition semantics as ``up``."""
overrides = _parse_set_overrides(set_values)
feature_state_kwargs = {} if validate_feature_states else {"validate_feature_states": False}
feature_state_kwargs = (
{} if validate_feature_states else {"validate_feature_states": False}
)
if environment:
return load_spec_with_environment(
spec_file,
Expand All @@ -95,7 +97,9 @@ def _load_spec_for_cli(
**feature_state_kwargs,
)
if overrides:
return load_spec_with_composition(spec_file, overrides=overrides, **feature_state_kwargs)
return load_spec_with_composition(
spec_file, overrides=overrides, **feature_state_kwargs
)
return load_spec(spec_file, **feature_state_kwargs)


Expand Down Expand Up @@ -478,7 +482,16 @@ async def _readiness(
group="spec",
)
)
ctx = build_context(db_url, state_file, skip_db=skip_db)
ctx = build_context(
db_url,
state_file,
skip_db=skip_db,
spec_subnets=tuple(
str(network.subnet)
for network in spec.substrate.networks.values()
if getattr(network, "subnet", None)
),
)
results.extend(run_preflight(ctx))
results.extend(await _check_migration_readiness(db_url))
results.extend(_feature_state_readiness_results(spec))
Expand Down Expand Up @@ -549,7 +562,11 @@ def validate(
)
except SpecLoadError as exc:
if output_format == "json":
click.echo(json.dumps({"ok": False, "error": str(exc), "feature_states": []}, indent=2))
click.echo(
json.dumps(
{"ok": False, "error": str(exc), "feature_states": []}, indent=2
)
)
else:
click.echo(f"Spec validation failed: {exc}", err=True)
sys.exit(1)
Expand All @@ -570,7 +587,9 @@ def validate(
)
else:
if unsupported:
click.echo("Spec validation failed: Unsupported spec features enabled:", err=True)
click.echo(
"Spec validation failed: Unsupported spec features enabled:", err=True
)
for item in unsupported:
click.echo(
f" - {item['path']} is {item['state']} in {item['stage']}: {item['reason']}",
Expand Down
Loading