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
2 changes: 1 addition & 1 deletion docs/LINUX_SERVER_DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ The default compose deployment binds localhost only, uses runtime/log volumes, a
```bash
python -m pip check
python -m unittest discover -s tests -v
python scripts/qa/server_deployment_smoke.py
cd frontend && npm ci && npm run test:ui && npm run build
cd ../desktop/electron && npm ci && npm audit --omit=dev
cd ../..
Expand All @@ -193,4 +194,3 @@ These checks do not require root, Docker daemon access, live capture, Nginx, Cad
- Nginx/Caddy proxy issue: verify backend health on `127.0.0.1:8000`.
- High CPU/RAM pressure: run the CI-safe benchmark and inspect `/api/monitoring/metrics`.
- Benchmark degraded: treat it as sizing guidance, then lower capture/load or increase CPU/RAM.

2 changes: 2 additions & 0 deletions docs/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Use this checklist before tagging or publishing a release candidate.
`python benchmarks/soak_test_pipeline.py --duration-sec 10 --events-per-sec 200 --flows 20 --ci-safe --output .runtime/benchmarks/release-hardening-smoke`
- [ ] Run the CI-safe real-load benchmark:
`python benchmarks/long_soak_runner.py --profile light_desktop --duration-sec 5 --events-per-sec 100 --flows 10 --websocket-clients 1 --sample-interval-sec 0.25 --max-cpu-avg-percent 100 --max-cpu-peak-percent 1000 --ci-safe --output .runtime/benchmarks/release-real-load-smoke`
- [ ] Run server deployment static smoke:
`python scripts/qa/server_deployment_smoke.py`
- [ ] For release candidates, run at least one manual local profile from
`docs/REAL_LOAD_TESTING.md` on representative hardware.
- [ ] Confirm bounded queues and buffers remain visible in Ops Snapshot.
Expand Down
93 changes: 93 additions & 0 deletions scripts/qa/server_deployment_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import Any


REPO_ROOT = Path(__file__).resolve().parents[2]


REQUIRED_TEXT_CHECKS = (
("systemd_service_user", "deploy/systemd/netbotpro.service", "User=netbotpro"),
(
"systemd_environment_file",
"deploy/systemd/netbotpro.service",
"EnvironmentFile=/etc/netbotpro/netbotpro.env",
),
(
"server_profile_env",
"deploy/systemd/netbotpro.env.example",
"NETBOT_PROFILE=server",
),
(
"live_capture_disabled_by_default",
"deploy/systemd/netbotpro.env.example",
"NETBOT_ENABLE_LIVE_CAPTURE=false",
),
("docker_non_root_user", "Dockerfile", "USER netbotpro"),
("docker_healthcheck", "Dockerfile", "HEALTHCHECK"),
("compose_localhost_bind", "docker-compose.yml", '"127.0.0.1:8000:8000"'),
(
"linux_docs_safe_boundary",
"docs/LINUX_SERVER_DEPLOYMENT.md",
"not command/control",
),
)


def _read(relative_path: str) -> str:
return (REPO_ROOT / relative_path).read_text(encoding="utf-8")


def run_checks() -> dict[str, Any]:
results: list[dict[str, Any]] = []
for name, relative_path, expected in REQUIRED_TEXT_CHECKS:
content = _read(relative_path)
results.append({"name": name, "file": relative_path, "ok": expected in content})

systemd = _read("deploy/systemd/netbotpro.service")
results.append(
{
"name": "systemd_not_root",
"file": "deploy/systemd/netbotpro.service",
"ok": "User=root" not in systemd,
}
)

compose = _read("docker-compose.yml").lower()
results.append(
{
"name": "compose_not_privileged",
"file": "docker-compose.yml",
"ok": "privileged: true" not in compose,
}
)

dockerignore = _read(".dockerignore")
for ignored in (".runtime", "node_modules", "*.pcap", "*.pcapng", ".env"):
results.append(
{
"name": f"dockerignore_{ignored}",
"file": ".dockerignore",
"ok": ignored in dockerignore,
}
)

failed = [item for item in results if not item["ok"]]
return {
"ok": not failed,
"checks_total": len(results),
"checks_failed": len(failed),
"failed": failed,
}


def main() -> int:
result = run_checks()
print(json.dumps(result, indent=2, ensure_ascii=True))
return 0 if result["ok"] else 1


if __name__ == "__main__":
raise SystemExit(main())
16 changes: 16 additions & 0 deletions tests/test_server_deployment_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import unittest

from scripts.qa.server_deployment_smoke import run_checks


class ServerDeploymentSmokeTests(unittest.TestCase):
def test_server_deployment_smoke_checks_pass(self):
result = run_checks()

self.assertTrue(result["ok"], result["failed"])
self.assertGreaterEqual(result["checks_total"], 10)
self.assertEqual(result["checks_failed"], 0)


if __name__ == "__main__":
unittest.main()