Spaces:
Sleeping
Sleeping
Graham Paasch commited on
Commit ·
0d737a7
1
Parent(s): e319d01
Bring up WireGuard when provided and add SSH preflight summary
Browse files- agent/pipeline_engine.py +78 -2
- app.py +9 -0
agent/pipeline_engine.py
CHANGED
|
@@ -21,6 +21,9 @@ import yaml
|
|
| 21 |
import logging
|
| 22 |
import os
|
| 23 |
import time
|
|
|
|
|
|
|
|
|
|
| 24 |
from distutils.util import strtobool
|
| 25 |
from .netbox_client import NetBoxClient
|
| 26 |
from agent.network_ops import create_gns3_project
|
|
@@ -221,6 +224,7 @@ class OvergrowthPipeline:
|
|
| 221 |
# Enable deployment by default; set OG_DEPLOY_ENABLED=0 to disable in restricted envs.
|
| 222 |
self.deploy_enabled = strtobool(os.getenv("OG_DEPLOY_ENABLED", "1")) == 1
|
| 223 |
self.ssh_on_seed_fail = strtobool(os.getenv("OG_SSH_ON_SEED_FAIL", "0")) == 1
|
|
|
|
| 224 |
|
| 225 |
def _slugify(self, text: str, length: int = 24) -> str:
|
| 226 |
import re
|
|
@@ -229,6 +233,45 @@ class OvergrowthPipeline:
|
|
| 229 |
slug = "network"
|
| 230 |
return slug[:length]
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
def _prepare_lab_info(self, intent: NetworkIntent, create_project: bool = False) -> Dict[str, Any]:
|
| 233 |
"""
|
| 234 |
Return lab link info and optionally create a project via MCP to get the project_id.
|
|
@@ -274,6 +317,26 @@ class OvergrowthPipeline:
|
|
| 274 |
lab_info["web_url"] = f"{self.gns3_server.rstrip('/')}/static/webUi"
|
| 275 |
|
| 276 |
return lab_info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
def _build_model_from_dict(self, data: Dict[str, Any], description: str,
|
| 279 |
constraints: Optional[List[str]] = None) -> NetworkModel:
|
|
@@ -1267,6 +1330,17 @@ Be specific and practical. Use RFC1918 addressing. Consider scalability and secu
|
|
| 1267 |
"name": lab_info.get("project_name"),
|
| 1268 |
"id": lab_info.get("project_id")
|
| 1269 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
|
| 1271 |
from agent.deployment_engine import DeploymentEngine
|
| 1272 |
|
|
@@ -1362,7 +1436,8 @@ Be specific and practical. Use RFC1918 addressing. Consider scalability and secu
|
|
| 1362 |
credentials=credentials,
|
| 1363 |
dry_run=dry_run,
|
| 1364 |
pre_checks=default_pre_checks,
|
| 1365 |
-
post_checks=default_post_checks
|
|
|
|
| 1366 |
)
|
| 1367 |
|
| 1368 |
results.append({
|
|
@@ -1401,7 +1476,8 @@ Be specific and practical. Use RFC1918 addressing. Consider scalability and secu
|
|
| 1401 |
'results': results,
|
| 1402 |
'summary': summary,
|
| 1403 |
'seed_results': seed_results,
|
| 1404 |
-
'deploy_mode': deploy_mode
|
|
|
|
| 1405 |
}
|
| 1406 |
|
| 1407 |
def stage7_observability(self, model: NetworkModel) -> Dict[str, Any]:
|
|
|
|
| 21 |
import logging
|
| 22 |
import os
|
| 23 |
import time
|
| 24 |
+
import base64
|
| 25 |
+
import subprocess
|
| 26 |
+
import socket
|
| 27 |
from distutils.util import strtobool
|
| 28 |
from .netbox_client import NetBoxClient
|
| 29 |
from agent.network_ops import create_gns3_project
|
|
|
|
| 224 |
# Enable deployment by default; set OG_DEPLOY_ENABLED=0 to disable in restricted envs.
|
| 225 |
self.deploy_enabled = strtobool(os.getenv("OG_DEPLOY_ENABLED", "1")) == 1
|
| 226 |
self.ssh_on_seed_fail = strtobool(os.getenv("OG_SSH_ON_SEED_FAIL", "0")) == 1
|
| 227 |
+
self._ensure_wireguard()
|
| 228 |
|
| 229 |
def _slugify(self, text: str, length: int = 24) -> str:
|
| 230 |
import re
|
|
|
|
| 233 |
slug = "network"
|
| 234 |
return slug[:length]
|
| 235 |
|
| 236 |
+
def _ensure_wireguard(self):
|
| 237 |
+
"""
|
| 238 |
+
If WG_CONFIG_B64 is provided, bring up wg0 using that config.
|
| 239 |
+
"""
|
| 240 |
+
wg_cfg_b64 = os.getenv("WG_CONFIG_B64")
|
| 241 |
+
if not wg_cfg_b64:
|
| 242 |
+
return
|
| 243 |
+
# If wg0 already exists, skip
|
| 244 |
+
try:
|
| 245 |
+
already_up = subprocess.run(
|
| 246 |
+
["ip", "link", "show", "wg0"],
|
| 247 |
+
stdout=subprocess.DEVNULL,
|
| 248 |
+
stderr=subprocess.DEVNULL,
|
| 249 |
+
timeout=2,
|
| 250 |
+
)
|
| 251 |
+
if already_up.returncode == 0:
|
| 252 |
+
logger.info("WireGuard wg0 already up; skipping bring-up")
|
| 253 |
+
return
|
| 254 |
+
except Exception:
|
| 255 |
+
pass
|
| 256 |
+
|
| 257 |
+
try:
|
| 258 |
+
cfg_bytes = base64.b64decode(wg_cfg_b64)
|
| 259 |
+
cfg_path = Path("/tmp/wg0.conf")
|
| 260 |
+
cfg_path.write_bytes(cfg_bytes)
|
| 261 |
+
logger.info("Bringing up WireGuard interface wg0 from WG_CONFIG_B64")
|
| 262 |
+
result = subprocess.run(
|
| 263 |
+
["wg-quick", "up", str(cfg_path)],
|
| 264 |
+
capture_output=True,
|
| 265 |
+
text=True,
|
| 266 |
+
timeout=15,
|
| 267 |
+
)
|
| 268 |
+
if result.returncode != 0:
|
| 269 |
+
logger.error(f"WireGuard bring-up failed: {result.stderr.strip()}")
|
| 270 |
+
else:
|
| 271 |
+
logger.info("WireGuard wg0 up")
|
| 272 |
+
except Exception as e:
|
| 273 |
+
logger.error(f"WireGuard setup error: {e}")
|
| 274 |
+
|
| 275 |
def _prepare_lab_info(self, intent: NetworkIntent, create_project: bool = False) -> Dict[str, Any]:
|
| 276 |
"""
|
| 277 |
Return lab link info and optionally create a project via MCP to get the project_id.
|
|
|
|
| 317 |
lab_info["web_url"] = f"{self.gns3_server.rstrip('/')}/static/webUi"
|
| 318 |
|
| 319 |
return lab_info
|
| 320 |
+
|
| 321 |
+
def _ssh_preflight(self, model: NetworkModel, port: int = 22, timeout: int = 3) -> List[Dict[str, Any]]:
|
| 322 |
+
"""Attempt TCP connection to each device mgmt_ip to surface reachability before deployment."""
|
| 323 |
+
results = []
|
| 324 |
+
for device in model.devices:
|
| 325 |
+
ip = getattr(device, "mgmt_ip", None)
|
| 326 |
+
status = {"device": device.name, "ip": ip, "port": port}
|
| 327 |
+
if not ip:
|
| 328 |
+
status["status"] = "missing_ip"
|
| 329 |
+
status["error"] = "No mgmt_ip set"
|
| 330 |
+
results.append(status)
|
| 331 |
+
continue
|
| 332 |
+
try:
|
| 333 |
+
with socket.create_connection((ip, port), timeout=timeout):
|
| 334 |
+
status["status"] = "reachable"
|
| 335 |
+
except Exception as e:
|
| 336 |
+
status["status"] = "unreachable"
|
| 337 |
+
status["error"] = str(e)
|
| 338 |
+
results.append(status)
|
| 339 |
+
return results
|
| 340 |
|
| 341 |
def _build_model_from_dict(self, data: Dict[str, Any], description: str,
|
| 342 |
constraints: Optional[List[str]] = None) -> NetworkModel:
|
|
|
|
| 1330 |
"name": lab_info.get("project_name"),
|
| 1331 |
"id": lab_info.get("project_id")
|
| 1332 |
}
|
| 1333 |
+
|
| 1334 |
+
# Deployment credentials from environment
|
| 1335 |
+
env_username = os.getenv("DEPLOY_USERNAME")
|
| 1336 |
+
env_password = os.getenv("DEPLOY_PASSWORD")
|
| 1337 |
+
env_port = int(os.getenv("DEPLOY_PORT", "22"))
|
| 1338 |
+
if credentials is None:
|
| 1339 |
+
credentials = {
|
| 1340 |
+
'username': env_username or 'admin',
|
| 1341 |
+
'password': env_password or 'admin'
|
| 1342 |
+
}
|
| 1343 |
+
ssh_preflight = self._ssh_preflight(model, port=env_port)
|
| 1344 |
|
| 1345 |
from agent.deployment_engine import DeploymentEngine
|
| 1346 |
|
|
|
|
| 1436 |
credentials=credentials,
|
| 1437 |
dry_run=dry_run,
|
| 1438 |
pre_checks=default_pre_checks,
|
| 1439 |
+
post_checks=default_post_checks,
|
| 1440 |
+
port=env_port
|
| 1441 |
)
|
| 1442 |
|
| 1443 |
results.append({
|
|
|
|
| 1476 |
'results': results,
|
| 1477 |
'summary': summary,
|
| 1478 |
'seed_results': seed_results,
|
| 1479 |
+
'deploy_mode': deploy_mode,
|
| 1480 |
+
'ssh_preflight': ssh_preflight
|
| 1481 |
}
|
| 1482 |
|
| 1483 |
def stage7_observability(self, model: NetworkModel) -> Dict[str, Any]:
|
app.py
CHANGED
|
@@ -465,6 +465,15 @@ def build_ui():
|
|
| 465 |
status += f"- Web UI: {gns3_web}\n"
|
| 466 |
status += "\n"
|
| 467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
# Pre-flight validation section
|
| 469 |
if ready_to_deploy:
|
| 470 |
status += "### ✅ Pre-flight Validation PASSED\n"
|
|
|
|
| 465 |
status += f"- Web UI: {gns3_web}\n"
|
| 466 |
status += "\n"
|
| 467 |
|
| 468 |
+
# SSH preflight summary (if available)
|
| 469 |
+
preflight_ssh = results.get("deployment", {}).get("ssh_preflight") if isinstance(results.get("deployment"), dict) else None
|
| 470 |
+
if preflight_ssh:
|
| 471 |
+
reachable = len([r for r in preflight_ssh if r.get("status") == "reachable"])
|
| 472 |
+
unreachable = len([r for r in preflight_ssh if r.get("status") == "unreachable"])
|
| 473 |
+
status += "### 🔌 SSH Reachability\n"
|
| 474 |
+
status += f"- Reachable: {reachable}\n"
|
| 475 |
+
status += f"- Unreachable: {unreachable}\n\n"
|
| 476 |
+
|
| 477 |
# Pre-flight validation section
|
| 478 |
if ready_to_deploy:
|
| 479 |
status += "### ✅ Pre-flight Validation PASSED\n"
|