Skip to content
4 changes: 2 additions & 2 deletions netengine/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ async def require_auth(
with _keycloak_ssl_context(state) as ssl_context:
async with session.post(
f"{KEYCLOAK_ISSUER}/protocol/openid-connect/token/introspect",
data={"token": token},
auth=aiohttp.BasicAuth("admin-cli", admin_password),
data={"token": token, "client_id": client_id},
auth=aiohttp.BasicAuth(client_id, client_secret),
ssl=ssl_context,
) as resp:
if resp.status != 200:
Expand Down
2 changes: 1 addition & 1 deletion netengine/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from netengine.logging import get_logger
from netengine.phase_labels import PHASE_LABELS
from netengine.spec.loader import SpecLoadError, load_spec
from netengine.spec.models import NetEngineSpec, SPEC_SCHEMA_VERSION
from netengine.spec.models import SPEC_SCHEMA_VERSION, NetEngineSpec

logger = get_logger(__name__)
router = APIRouter(prefix="/api/v1")
Expand Down
4 changes: 2 additions & 2 deletions netengine/cli/init_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def _core_host(host: int, subnet: str) -> str:
"nat": False,
"dynamic_ip": False,
"inbound": "allowed",
"reverse_dns": True,
"reverse_dns": False,
},
"residential": {
"dhcp": True,
Expand All @@ -428,7 +428,7 @@ def _core_host(host: int, subnet: str) -> str:
"nat": False,
"dynamic_ip": False,
"inbound": "allowed",
"reverse_dns": True,
"reverse_dns": False,
},
"airgapped": {
"dhcp": True,
Expand Down
2 changes: 1 addition & 1 deletion netengine/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ def import_support_bundle(bundle_file: str) -> None:

from netengine.api.routes import SUPPORTED_IMPORT_SCHEMA_VERSIONS, _validate_import_phase_state
from netengine.core.state import RuntimeState
from netengine.spec.models import NetEngineSpec, SUPPORTED_SPEC_SCHEMA_VERSIONS
from netengine.spec.models import SUPPORTED_SPEC_SCHEMA_VERSIONS, NetEngineSpec

body = _json.loads(Path(bundle_file).read_text())
if body.get("schema_version") not in SUPPORTED_IMPORT_SCHEMA_VERSIONS:
Expand Down
2 changes: 1 addition & 1 deletion netengine/core/docker_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def create_docker_client() -> tuple[Optional[Any], bool]:
try:
from netengine.handlers.docker_handler import DockerHandler

client: Any = DockerHandler() # type: ignore[no-untyped-call]
client: Any = DockerHandler()
return client, False
except Exception as exc:
logger.warning(f"Docker unavailable, falling back to mock mode: {exc}")
Expand Down
22 changes: 16 additions & 6 deletions netengine/core/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ def discover(self) -> list[MigrationFile]:
version, _, rest = path.stem.partition("_")
name = rest or path.stem
checksum = hashlib.sha256(path.read_bytes()).hexdigest()
migrations.append(MigrationFile(version=version, name=name, path=path, checksum=checksum))
migrations.append(
MigrationFile(version=version, name=name, path=path, checksum=checksum)
)
return migrations

async def apply_pending(self) -> list[MigrationRecord]:
"""Apply pending migrations and return records applied in this invocation."""
import asyncpg # type: ignore[import]
import asyncpg # type: ignore[import-untyped]

conn = await asyncpg.connect(self.db_url)
try:
Expand Down Expand Up @@ -111,7 +113,7 @@ async def apply_pending(self) -> list[MigrationRecord]:
await conn.close()

async def status(self) -> MigrationStatus:
import asyncpg # type: ignore[import]
import asyncpg

conn = await asyncpg.connect(self.db_url)
try:
Expand All @@ -132,7 +134,9 @@ async def status(self) -> MigrationStatus:

pgmq_available = await self._pgmq_available(conn)
pgmq_installed = await self._pgmq_installed(conn)
missing_queues = await self._missing_queues(conn) if pgmq_installed else [q.value for q in Queue]
missing_queues = (
await self._missing_queues(conn) if pgmq_installed else [q.value for q in Queue]
)

return MigrationStatus(
applied=[record for record in records if record.status == "applied"],
Expand Down Expand Up @@ -203,10 +207,16 @@ async def _upsert_record(
)

async def _pgmq_available(self, conn: Any) -> bool:
return bool(await conn.fetchval("SELECT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pgmq')"))
return bool(
await conn.fetchval(
"SELECT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pgmq')"
)
)

async def _pgmq_installed(self, conn: Any) -> bool:
return bool(await conn.fetchval("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgmq')"))
return bool(
await conn.fetchval("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgmq')")
)

async def _missing_queues(self, conn: Any) -> list[str]:
rows = await conn.fetch(
Expand Down
4 changes: 3 additions & 1 deletion netengine/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ def _discard_completion_flags_without_outputs(self) -> None:
"9": (self.org_apps_output,),
}
for phase, outputs in phase_outputs.items():
if self.phase_completed.get(phase) is True and any(output is None for output in outputs):
if self.phase_completed.get(phase) is True and any(
output is None for output in outputs
):
self.phase_completed.pop(phase, None)

if self.pki_output is None and self.phase_completed.get("3") is True:
Expand Down
7 changes: 4 additions & 3 deletions netengine/db/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def run_migrations(
migrations_dir: Path | str = MIGRATIONS_DIR,
) -> MigrationRunResult:
"""Apply all pending SQL migrations and return structured outcomes."""
import asyncpg # type: ignore[import]
import asyncpg # type: ignore[import-untyped]

resolved_dir = Path(migrations_dir)
migration_files = discover_migrations(resolved_dir)
Expand Down Expand Up @@ -233,7 +233,7 @@ async def migration_status(
migrations_dir: Path | str = MIGRATIONS_DIR,
) -> MigrationStatusReport:
"""Inspect migration state without applying pending migration files."""
import asyncpg # type: ignore[import]
import asyncpg

resolved_dir = Path(migrations_dir)
migration_files = discover_migrations(resolved_dir)
Expand All @@ -251,7 +251,8 @@ async def migration_status(
checksum = migration_checksum(sql)
filename = migration_path.name
existing = await conn.fetchrow(
"SELECT checksum, success, applied_at, error FROM schema_migrations WHERE filename = $1",
"SELECT checksum, success, applied_at, error"
" FROM schema_migrations WHERE filename = $1",
filename,
)
if not existing:
Expand Down
99 changes: 95 additions & 4 deletions netengine/diagnostic/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from typing import Callable, Iterable
from typing import Any, Callable, Iterable
from urllib.parse import urlparse

import click
Expand Down Expand Up @@ -72,7 +72,7 @@ def _run(command: list[str], *, timeout: float = 8.0) -> subprocess.CompletedPro
return subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False)


def _compose_config(project_root: Path | None = None) -> dict:
def _compose_config(project_root: Path | None = None) -> dict[str, Any]:
compose_file = (project_root or _repo_root()) / "docker-compose.yml"
try:
return yaml.safe_load(compose_file.read_text()) or {}
Expand Down Expand Up @@ -107,7 +107,8 @@ def _check_python() -> DoctorCheckResult:
return DoctorCheckResult(
"Python runtime",
DoctorStatus.OK if ok else DoctorStatus.FAIL,
f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}; pyproject requires ^3.13",
f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro};"
" pyproject requires ^3.13",
None if ok else "Install Python 3.13 or newer and recreate the virtualenv.",
"host",
)
Expand Down Expand Up @@ -316,6 +317,94 @@ def _docker_names(kind: str) -> set[str]:
)


def _check_docker_subnet_conflicts(ctx: DoctorContext) -> DoctorCheckResult:
"""Warn if any existing Docker network subnets overlap with NetEngine's configured subnets."""
compose_config = _compose_config(ctx.project_root)
ne_subnets: list[str] = []
for network_cfg in (compose_config.get("networks") or {}).values():
if isinstance(network_cfg, dict):
for ipam_config in (network_cfg.get("ipam") or {}).get("config") or []:
if isinstance(ipam_config, dict) and "subnet" in ipam_config:
ne_subnets.append(ipam_config["subnet"])

if not ne_subnets:
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.SKIP,
"no subnets defined in compose config",
group="docker",
required=False,
)

try:
import ipaddress

result = _run(["docker", "network", "ls", "-q"])
if result.returncode != 0 or not result.stdout.strip():
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.OK,
"no existing Docker networks to check",
group="docker",
)
network_ids = result.stdout.split()
inspect_result = _run(["docker", "network", "inspect"] + network_ids)
if inspect_result.returncode != 0:
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.WARN,
"could not inspect Docker networks",
"Run `docker network ls` manually to check for subnet conflicts.",
"docker",
required=False,
)

import json as _json

networks = _json.loads(inspect_result.stdout)
ne_networks = [ipaddress.ip_network(s, strict=False) for s in ne_subnets]
conflicts: list[str] = []
for network in networks:
name = network.get("Name", "unknown")
for ipam_config in (network.get("IPAM") or {}).get("Config") or []:
subnet_str = ipam_config.get("Subnet")
if not subnet_str:
continue
try:
existing = ipaddress.ip_network(subnet_str, strict=False)
except ValueError:
continue
for ne_net in ne_networks:
if existing.overlaps(ne_net):
conflicts.append(f"{name} ({subnet_str}) overlaps {ne_net}")

if conflicts:
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.WARN,
"; ".join(conflicts),
"Remove conflicting networks with"
" `docker network rm <name>` or `docker network prune`.",
"docker",
required=False,
)
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.OK,
f"no conflicts with {', '.join(ne_subnets)}",
group="docker",
)
except Exception as exc:
return DoctorCheckResult(
"Docker subnet conflicts",
DoctorStatus.WARN,
f"subnet conflict check failed: {exc}",
"Run `docker network ls` manually to check for subnet conflicts.",
"docker",
required=False,
)


def _check_docker_conflicts(ctx: DoctorContext) -> list[DoctorCheckResult]:
_, containers, volumes, networks = _compose_ports_and_resources(ctx.project_root)
checks = []
Expand All @@ -327,7 +416,8 @@ def _check_docker_conflicts(ctx: DoctorContext) -> list[DoctorCheckResult]:
DoctorStatus.WARN if conflicts else DoctorStatus.OK,
", ".join(conflicts) if conflicts else "no known name conflicts",
(
"Run `netengine down` or remove stale Docker resources if these belong to an old run."
"Run `netengine down` or remove stale Docker resources"
" if these belong to an old run."
if conflicts
else None
),
Expand Down Expand Up @@ -421,6 +511,7 @@ def standard_probes() -> tuple[DoctorProbe, ...]:
_check_ports,
_check_filesystem,
_check_docker_conflicts,
lambda ctx: _check_docker_subnet_conflicts(ctx),
)


Expand Down
1 change: 0 additions & 1 deletion netengine/events/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Any

from netengine.core.state import EventSendFailure

from netengine.events import factory as event_factory
from netengine.events.queues import Queue, queue_for_event_type
from netengine.events.schema import EventEnvelope
Expand Down
2 changes: 1 addition & 1 deletion netengine/handlers/and_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from netengine.events.schema import EventEnvelope
from netengine.handlers.context import PhaseContext
from netengine.handlers.dns import DNSHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.handlers.domain_registry_handler import DomainRegistryHandler
from netengine.handlers.gateway_handler import GatewayHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.logging import get_logger


Expand Down
2 changes: 1 addition & 1 deletion netengine/handlers/app_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from netengine.handlers._base import BasePhaseHandler
from netengine.handlers.context import PhaseContext
from netengine.handlers.dns import DNSHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.handlers.oidc_handler import OIDCHandler
from netengine.handlers.pki_handler import PKIHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.logging import get_logger


Expand Down
3 changes: 1 addition & 2 deletions netengine/handlers/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional

from netengine.handlers.protocols import DockerAdapterProtocol

from netengine.core.state import RuntimeState
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.spec.models import NetEngineSpec

if TYPE_CHECKING:
Expand Down
4 changes: 2 additions & 2 deletions netengine/handlers/gateway_portal_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
from typing import Any

from netengine.errors import GatewayError, PKIError
from netengine.events.queues import Queue
from netengine.events.emitter import emit_event
from netengine.events.queues import Queue
from netengine.handlers._base import BasePhaseHandler
from netengine.handlers.context import PhaseContext
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.handlers.gateway_handler import GatewayHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.logging import get_logger
from netengine.spec.models import CrossWorldPeer, GatewayPortal
from netengine.spec.types import GatewayCrossWorldMode, GatewayRealInternetMode
Expand Down
2 changes: 1 addition & 1 deletion netengine/handlers/minio_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from datetime import UTC, datetime

from netengine.handlers.dns import DNSHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.handlers.pki_handler import PKIHandler
from netengine.handlers.protocols import DockerAdapterProtocol


class StorageHandler:
Expand Down
2 changes: 1 addition & 1 deletion netengine/handlers/phase_pki.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from netengine.events.emitter import emit_event
from netengine.handlers._base import BasePhaseHandler
from netengine.handlers.context import PhaseContext
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.handlers.pki_handler import PKIHandler
from netengine.handlers.protocols import DockerAdapterProtocol
from netengine.logging import get_logger
from netengine.workers.pki_cert_rotation_worker import CertTypeRotationConfig, PKICertRotationWorker

Expand Down
2 changes: 1 addition & 1 deletion netengine/handlers/substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from datetime import UTC, datetime
from typing import Any, cast

from netengine.core.state import SubstrateOutput
from netengine.errors import SubstrateError
from netengine.events.emitter import emit_event
from netengine.core.state import SubstrateOutput
from netengine.handlers._base import BasePhaseHandler
from netengine.handlers.context import PhaseContext

Expand Down
3 changes: 2 additions & 1 deletion netengine/logging/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ def format_record_json(record: "Record") -> str:
"parent_span_id": record["extra"].get("parent_span_id"),
}

return json.dumps(log_entry) + "\n"
# Escape braces so loguru's format_map pass doesn't treat JSON keys as placeholders.
return json.dumps(log_entry).replace("{", "{{").replace("}", "}}") + "\n"


# ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion netengine/phases/phase_ands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from datetime import UTC, datetime
from typing import Any, Optional

from netengine.events.queues import Queue
from netengine.events.emitter import emit_event
from netengine.events.queues import Queue
from netengine.events.schema import EventEnvelope
from netengine.handlers._base import BasePhaseHandler
from netengine.handlers.context import PhaseContext
Expand Down
Loading
Loading