Graham Paasch commited on
Commit
0d737a7
·
1 Parent(s): e319d01

Bring up WireGuard when provided and add SSH preflight summary

Browse files
Files changed (2) hide show
  1. agent/pipeline_engine.py +78 -2
  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"