Initial documentation structure
This commit is contained in:
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# SRX AI-Driven Network Automation System
|
||||||
|
|
||||||
|
## Production System Documentation
|
||||||
|
|
||||||
|
This repository documents the AI-driven network automation system managing Juniper SRX firewall configurations.
|
||||||
|
|
||||||
|
### System Components
|
||||||
|
- **Orchestrator VM** (192.168.100.87): Main automation hub
|
||||||
|
- **AI Processor VM** (192.168.100.86): Ollama llama2:13b
|
||||||
|
- **Elasticsearch VM** (192.168.100.85): NetFlow analytics
|
||||||
|
- **Gitea Server** (git.salmutt.dev): Git repository and PR management
|
||||||
|
- **Juniper SRX** (192.168.100.1): Target firewall
|
||||||
|
|
||||||
|
### Key Metrics
|
||||||
|
- 850,000+ NetFlow records processed daily
|
||||||
|
- 19+ days uptime (AI processor)
|
||||||
|
- 8 security zones configured
|
||||||
|
- 100% syntax accuracy after feedback learning
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
- [Architecture Overview](docs/architecture/README.md)
|
||||||
|
- [Operations Guide](docs/operations/README.md)
|
||||||
|
- [API Reference](docs/api/README.md)
|
||||||
|
- [Script Inventory](scripts/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Repository maintained by: netops*
|
||||||
|
*Last Updated: September 2025*
|
||||||
41
collect_scripts.sh
Executable file
41
collect_scripts.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Collecting production scripts..."
|
||||||
|
|
||||||
|
# Create directories for organized scripts
|
||||||
|
mkdir -p scripts/orchestrator/{core,pipeline,gitea,srx,monitoring,utilities}
|
||||||
|
|
||||||
|
# Core orchestrator files
|
||||||
|
echo "Collecting core orchestrator files..."
|
||||||
|
if [ -f ~/orchestrator/orchestrator_main.py ]; then
|
||||||
|
cp ~/orchestrator/orchestrator_main.py scripts/orchestrator/core/
|
||||||
|
cp ~/orchestrator/requirements.txt scripts/orchestrator/core/
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configuration (sanitized)
|
||||||
|
if [ -f ~/orchestrator/config.yaml ]; then
|
||||||
|
cp ~/orchestrator/config.yaml scripts/orchestrator/core/config.yaml.template
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pipeline scripts
|
||||||
|
echo "Collecting pipeline scripts..."
|
||||||
|
for script in run_pipeline.py prepare_pr.py create_ai_pr.py; do
|
||||||
|
[ -f ~/orchestrator/$script ] && cp ~/orchestrator/$script scripts/orchestrator/pipeline/
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gitea integration scripts
|
||||||
|
for script in webhook_listener.py gitea_pr_feedback.py close_pr_with_feedback.py; do
|
||||||
|
[ -f ~/orchestrator/$script ] && cp ~/orchestrator/$script scripts/orchestrator/gitea/
|
||||||
|
done
|
||||||
|
|
||||||
|
# SRX management scripts
|
||||||
|
for script in collect_srx_config.py srx_manager.py deploy_approved.py; do
|
||||||
|
[ -f ~/orchestrator/$script ] && cp ~/orchestrator/$script scripts/orchestrator/srx/
|
||||||
|
done
|
||||||
|
|
||||||
|
# Service files
|
||||||
|
sudo cp /etc/systemd/system/orchestrator.service infrastructure/systemd/ 2>/dev/null
|
||||||
|
sudo cp /etc/systemd/system/gitea-webhook.service infrastructure/systemd/ 2>/dev/null
|
||||||
|
sudo chown $USER:$USER infrastructure/systemd/*.service 2>/dev/null
|
||||||
|
|
||||||
|
echo "Collection complete!"
|
||||||
50
docs/operations/README.md
Normal file
50
docs/operations/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Operations Guide
|
||||||
|
|
||||||
|
## Daily Operations
|
||||||
|
|
||||||
|
### Morning Health Check
|
||||||
|
|
||||||
|
Check services:
|
||||||
|
systemctl status orchestrator
|
||||||
|
systemctl status gitea-webhook
|
||||||
|
|
||||||
|
Check for pending PRs:
|
||||||
|
curl -s https://git.salmutt.dev/api/v1/repos/sal/srx-config/pulls?state=open
|
||||||
|
|
||||||
|
View recent activity:
|
||||||
|
journalctl -u orchestrator --since "12 hours ago" | grep -E "anomaly|trigger"
|
||||||
|
|
||||||
|
### Manual Operations
|
||||||
|
|
||||||
|
Trigger Analysis:
|
||||||
|
cd /home/netops/orchestrator
|
||||||
|
python3 run_pipeline.py --context security --timeout 120
|
||||||
|
|
||||||
|
View AI Responses:
|
||||||
|
ls -lt /shared/ai-gitops/responses/ | head -10
|
||||||
|
|
||||||
|
Check Learning Patterns:
|
||||||
|
cat /shared/ai-gitops/learning/patterns.json
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Service Issues
|
||||||
|
|
||||||
|
Check logs:
|
||||||
|
journalctl -u orchestrator -n 50
|
||||||
|
|
||||||
|
Verify NFS mount:
|
||||||
|
df -h | grep shared
|
||||||
|
|
||||||
|
Test Elasticsearch:
|
||||||
|
curl -s 192.168.100.85:9200/_cluster/health
|
||||||
|
|
||||||
|
### AI Not Responding
|
||||||
|
|
||||||
|
SSH to AI processor:
|
||||||
|
ssh netops@192.168.100.86
|
||||||
|
systemctl status ai-processor
|
||||||
|
systemctl status ollama
|
||||||
|
|
||||||
|
Test Ollama:
|
||||||
|
curl http://localhost:11434/api/tags
|
||||||
18
infrastructure/systemd/gitea-webhook.service
Normal file
18
infrastructure/systemd/gitea-webhook.service
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gitea Webhook Listener for AI Feedback
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=netops
|
||||||
|
WorkingDirectory=/home/netops/orchestrator
|
||||||
|
#ExecStart=/usr/bin/python3 /home/netops/orchestrator/webhook_listener.py
|
||||||
|
ExecStart=/home/netops/orchestrator/venv/bin/python /home/netops/orchestrator/webhook_listener.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
EnvironmentFile=-/home/netops/.env
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
47
infrastructure/systemd/orchestrator.service
Normal file
47
infrastructure/systemd/orchestrator.service
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Network AI Orchestrator Service
|
||||||
|
Documentation=https://git.salmutt.dev/sal/srx-config
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
RequiresMountsFor=/shared/ai-gitops
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=netops
|
||||||
|
Group=netops
|
||||||
|
WorkingDirectory=/home/netops/orchestrator
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
EnvironmentFile=-/home/netops/.env
|
||||||
|
|
||||||
|
# Python virtual environment activation and script execution
|
||||||
|
ExecStart=/home/netops/orchestrator/venv/bin/python /home/netops/orchestrator/orchestrator_main.py
|
||||||
|
|
||||||
|
# Restart configuration
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
StartLimitInterval=200
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
MemoryLimit=8G
|
||||||
|
CPUQuota=50%
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
|
Environment="ORCHESTRATOR_ENV=production"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=orchestrator
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
PrivateTmp=yes
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/shared/ai-gitops /home/netops/orchestrator/logs /var/lib/orchestrator /var/log/orchestrator
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
29
scripts/README.md
Normal file
29
scripts/README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Production Scripts Inventory
|
||||||
|
|
||||||
|
## Orchestrator Scripts
|
||||||
|
|
||||||
|
### Core Service
|
||||||
|
- orchestrator_main.py - Main orchestration loop
|
||||||
|
- config.yaml - Configuration file
|
||||||
|
- requirements.txt - Python dependencies
|
||||||
|
|
||||||
|
### Pipeline Scripts
|
||||||
|
- run_pipeline.py - Manual pipeline trigger with context
|
||||||
|
- prepare_pr.py - Prepares PR content from AI response
|
||||||
|
- create_ai_pr.py - Creates PR in Gitea
|
||||||
|
|
||||||
|
### Webhook Integration
|
||||||
|
- webhook_listener.py - Flask server listening on port 5000
|
||||||
|
- gitea_pr_feedback.py - Processes PR feedback
|
||||||
|
- deploy_approved.py - Deploys merged PRs to SRX
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
Manual security analysis:
|
||||||
|
python3 run_pipeline.py --context security --timeout 120
|
||||||
|
|
||||||
|
Create PR from latest response:
|
||||||
|
python3 prepare_pr.py && python3 create_ai_pr.py
|
||||||
|
|
||||||
|
Deploy approved configuration:
|
||||||
|
python3 deploy_approved.py
|
||||||
56
scripts/orchestrator/core/config.yaml.template
Normal file
56
scripts/orchestrator/core/config.yaml.template
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Network AI Orchestrator Configuration
|
||||||
|
elasticsearch:
|
||||||
|
host: "192.168.100.85:9200"
|
||||||
|
index: "netflow-*"
|
||||||
|
verify_certs: false
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
analysis:
|
||||||
|
interval_minutes: 60
|
||||||
|
window_hours: 168
|
||||||
|
min_traffic_bytes: 1000000
|
||||||
|
|
||||||
|
pr_creation:
|
||||||
|
enabled: true
|
||||||
|
frequency: "smart" # Options: weekly, daily, manual, smart
|
||||||
|
triggers:
|
||||||
|
- high_traffic anomaly #Create PR if traffic spike
|
||||||
|
- security_event #Create PR if security issue
|
||||||
|
- scheduled: "weekly"
|
||||||
|
thresholds:
|
||||||
|
traffic_spike: 200 #200% increase triggers PR
|
||||||
|
new_hosts: 10 #10+ new IPs triggers PR
|
||||||
|
day_of_week: "saturday" # 0=Monday, 6=Sunday
|
||||||
|
hour_of_day: 22 # 24-hour format (9 = 9 AM)
|
||||||
|
skip_if_pending: true # Don't create if PR already open
|
||||||
|
min_days_between: 7 # Minimum days between PRs
|
||||||
|
|
||||||
|
gitea:
|
||||||
|
url: "https://git.salmutt.dev"
|
||||||
|
repo: "sal/srx-config"
|
||||||
|
token: "${GITEA_TOKEN}" # Use actual token
|
||||||
|
branch: "main"
|
||||||
|
labels: ["ai-generated", "auto-config", "pending-review"]
|
||||||
|
|
||||||
|
srx:
|
||||||
|
host: "192.168.100.1"
|
||||||
|
port: 830
|
||||||
|
username: "netops"
|
||||||
|
ssh_key: "/home/netops/.ssh/srx_key"
|
||||||
|
|
||||||
|
shared_storage:
|
||||||
|
path: "/shared/ai-gitops"
|
||||||
|
|
||||||
|
state_tracking:
|
||||||
|
enabled: true
|
||||||
|
state_file: '/shared/ai-gitops/state/orchestrator_state.json'
|
||||||
|
track_pr_history: true
|
||||||
|
|
||||||
|
ai:
|
||||||
|
request_timeout: 120
|
||||||
|
max_retries: 3
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "INFO"
|
||||||
|
max_file_size: "100MB"
|
||||||
|
retention_days: 30
|
||||||
771
scripts/orchestrator/core/orchestrator_main.py
Normal file
771
scripts/orchestrator/core/orchestrator_main.py
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Enhanced Network AI Orchestrator - Production Version with Gitea Integration
|
||||||
|
Compatible with Elasticsearch 7.x
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import signal
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
import yaml
|
||||||
|
import uuid
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from elasticsearch import Elasticsearch # Using sync version for ES 7.x
|
||||||
|
from git import Repo
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Load environment variables from home directory .env
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
env_path = Path.home() / '.env' # This gets /home/netops/.env
|
||||||
|
load_dotenv(env_path)
|
||||||
|
|
||||||
|
# ADD THIS IMPORT - New for Phase 3
|
||||||
|
from gitea_integration import GiteaIntegration
|
||||||
|
|
||||||
|
# Configure production logging
|
||||||
|
def setup_logging():
|
||||||
|
"""Configure comprehensive logging for production"""
|
||||||
|
log_dir = Path("/home/netops/orchestrator/logs")
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
log_file = log_dir / f"orchestrator_{datetime.now().strftime('%Y%m%d')}.log"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(log_file),
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
class NetworkOrchestrator:
|
||||||
|
def __init__(self, config_path: str = "/home/netops/orchestrator/config.yaml"):
|
||||||
|
"""Initialize the orchestrator with configuration"""
|
||||||
|
self.config = self.load_config(config_path)
|
||||||
|
self.es_client = None
|
||||||
|
self.git_repo = None
|
||||||
|
self.running = True
|
||||||
|
self.shared_dir = Path("/shared/ai-gitops")
|
||||||
|
self.request_dir = self.shared_dir / "requests"
|
||||||
|
self.response_dir = self.shared_dir / "responses"
|
||||||
|
|
||||||
|
# ADD THIS - Initialize state for Phase 3
|
||||||
|
self.state = self.load_state()
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
self.request_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.response_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
logger.info("Orchestrator initialized")
|
||||||
|
|
||||||
|
def should_create_pr(self):
|
||||||
|
"""Check if we should create a PR based on schedule and state"""
|
||||||
|
if not self.config.get('pr_creation', {}).get('enabled', True):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Load state
|
||||||
|
state = self.load_state()
|
||||||
|
|
||||||
|
# Check if pending PR exists
|
||||||
|
if self.config['pr_creation'].get('skip_if_pending', True):
|
||||||
|
if state.get('pending_pr', False):
|
||||||
|
logger.info("Skipping PR - existing PR is pending")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check frequency
|
||||||
|
frequency = self.config['pr_creation'].get('frequency', 'weekly')
|
||||||
|
|
||||||
|
if frequency == 'weekly':
|
||||||
|
# Check if it's the right day and hour
|
||||||
|
now = datetime.now()
|
||||||
|
target_day = self.config['pr_creation'].get('day_of_week', 'saturday')
|
||||||
|
target_hour = self.config['pr_creation'].get('hour_of_day', 5)
|
||||||
|
|
||||||
|
# Convert day name to number
|
||||||
|
days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||||
|
target_day_num = days.index(target_day.lower())
|
||||||
|
|
||||||
|
# Check if it's the right day and hour
|
||||||
|
if now.weekday() != target_day_num or now.hour != target_hour:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check minimum days between PRs
|
||||||
|
if state.get('last_pr_created'):
|
||||||
|
last_pr_date = datetime.fromisoformat(state['last_pr_created'])
|
||||||
|
days_since = (datetime.now() - last_pr_date).days
|
||||||
|
min_days = self.config['pr_creation'].get('min_days_between', 7)
|
||||||
|
|
||||||
|
if days_since < min_days:
|
||||||
|
logger.info(f"Skipping PR - only {days_since} days since last PR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load_state(self):
|
||||||
|
"""Load orchestrator state"""
|
||||||
|
state_file = self.config.get('state_tracking', {}).get('state_file', '/var/lib/orchestrator/state.json')
|
||||||
|
if Path(state_file).exists():
|
||||||
|
with open(state_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def check_smart_triggers(self, traffic_data: Dict) -> bool:
|
||||||
|
"""Check if any smart triggers are met"""
|
||||||
|
if self.config['pr_creation'].get('frequency') != 'smart':
|
||||||
|
return False
|
||||||
|
|
||||||
|
triggers = self.config['pr_creation'].get('triggers', [])
|
||||||
|
thresholds = self.config['pr_creation'].get('thresholds', {})
|
||||||
|
|
||||||
|
# Load previous state for comparison
|
||||||
|
state = self.load_state()
|
||||||
|
last_data = state.get('last_traffic_data', {})
|
||||||
|
|
||||||
|
# Check traffic spike
|
||||||
|
if 'high_traffic_anomaly' in str(triggers):
|
||||||
|
current_flows = sum(
|
||||||
|
b['doc_count']
|
||||||
|
for b in traffic_data.get('top_talkers', {}).get('buckets', [])
|
||||||
|
)
|
||||||
|
last_flows = last_data.get('total_flows', current_flows)
|
||||||
|
|
||||||
|
if last_flows > 0:
|
||||||
|
spike_percent = ((current_flows - last_flows) / last_flows) * 100
|
||||||
|
if spike_percent >= thresholds.get('traffic_spike', 200):
|
||||||
|
logger.info(f"🚨 Traffic spike detected: {spike_percent:.1f}% increase!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check new hosts
|
||||||
|
if 'security_event' in str(triggers):
|
||||||
|
current_ips = set(
|
||||||
|
b['key']
|
||||||
|
for b in traffic_data.get('top_talkers', {}).get('buckets', [])
|
||||||
|
)
|
||||||
|
last_ips = set(last_data.get('top_ips', []))
|
||||||
|
|
||||||
|
new_hosts = current_ips - last_ips
|
||||||
|
if len(new_hosts) >= thresholds.get('new_hosts', 10):
|
||||||
|
logger.info(f"🚨 Security event: {len(new_hosts)} new hosts detected!")
|
||||||
|
logger.info(f" New IPs: {', '.join(list(new_hosts)[:5])}...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check weekly schedule fallback
|
||||||
|
if any('scheduled' in str(t) for t in triggers):
|
||||||
|
if self.should_create_pr(): # Use existing weekly logic
|
||||||
|
logger.info("📅 Weekly schedule triggered")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Save current data for next comparison
|
||||||
|
self.save_state({
|
||||||
|
'last_traffic_data': {
|
||||||
|
'total_flows': sum(
|
||||||
|
b['doc_count']
|
||||||
|
for b in traffic_data.get('top_talkers', {}).get('buckets', [])
|
||||||
|
),
|
||||||
|
'top_ips': [
|
||||||
|
b['key']
|
||||||
|
for b in traffic_data.get('top_talkers', {}).get('buckets', [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_state(self, updates):
|
||||||
|
"""Save orchestrator state"""
|
||||||
|
state_file = self.config.get('state_tracking', {}).get('state_file', '/var/lib/orchestrator/state.json')
|
||||||
|
state = self.load_state()
|
||||||
|
state.update(updates)
|
||||||
|
state['last_updated'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
with open(state_file, 'w') as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
def load_config(self, config_path: str) -> Dict:
|
||||||
|
"""Load configuration from YAML file"""
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
logger.info(f"Configuration loaded from {config_path}")
|
||||||
|
|
||||||
|
# Replace environment variables
|
||||||
|
if 'gitea' in config and 'token' in config['gitea']:
|
||||||
|
if config['gitea']['token'] == '${GITEA_TOKEN}':
|
||||||
|
config['gitea']['token'] = os.environ.get('GITEA_TOKEN', '')
|
||||||
|
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load config: {e}")
|
||||||
|
# Use defaults if config fails
|
||||||
|
return {
|
||||||
|
'elasticsearch': {
|
||||||
|
'host': '192.168.100.85:9200',
|
||||||
|
'index': 'netflow-*'
|
||||||
|
},
|
||||||
|
'analysis': {
|
||||||
|
'interval_minutes': 60,
|
||||||
|
'window_hours': 24
|
||||||
|
},
|
||||||
|
'gitea': {
|
||||||
|
'url': 'https://git.salmutt.dev',
|
||||||
|
'repo': 'sal/srx-config',
|
||||||
|
'token': os.environ.get('GITEA_TOKEN', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_elasticsearch(self):
|
||||||
|
"""Initialize Elasticsearch connection (synchronous for ES 7.x)"""
|
||||||
|
try:
|
||||||
|
es_config = self.config['elasticsearch']
|
||||||
|
self.es_client = Elasticsearch(
|
||||||
|
hosts=[es_config['host']],
|
||||||
|
verify_certs=False,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
# Test connection
|
||||||
|
info = self.es_client.info()
|
||||||
|
logger.info(f"Connected to Elasticsearch: {info['version']['number']}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to Elasticsearch: {e}")
|
||||||
|
self.es_client = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def collect_traffic_data(self) -> Dict:
|
||||||
|
"""Collect traffic data from Elasticsearch (synchronous)"""
|
||||||
|
if not self.es_client:
|
||||||
|
logger.warning("Elasticsearch not connected, using mock data")
|
||||||
|
return self.generate_mock_data()
|
||||||
|
|
||||||
|
try:
|
||||||
|
window_hours = self.config['analysis']['window_hours']
|
||||||
|
query = {
|
||||||
|
"query": {
|
||||||
|
"range": {
|
||||||
|
"@timestamp": {
|
||||||
|
"gte": f"now-{window_hours}h",
|
||||||
|
"lte": "now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": 0,
|
||||||
|
"aggs": {
|
||||||
|
"top_talkers": {
|
||||||
|
"terms": {
|
||||||
|
"field": "netflow.ipv4_src_addr",
|
||||||
|
"size": 20
|
||||||
|
},
|
||||||
|
"aggs": {
|
||||||
|
"bytes": {"sum": {"field": "netflow.in_bytes"}},
|
||||||
|
"packets": {"sum": {"field": "netflow.in_pkts"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"terms": {
|
||||||
|
"field": "netflow.protocol",
|
||||||
|
"size": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"top_destinations": {
|
||||||
|
"terms": {
|
||||||
|
"field": "netflow.ipv4_dst_addr", # NEW
|
||||||
|
"size": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vlans": {
|
||||||
|
"terms": {
|
||||||
|
"field": "vlan_id",
|
||||||
|
"size": 20
|
||||||
|
},
|
||||||
|
"aggs": {
|
||||||
|
"bytes": {"sum": {"field": "bytes"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hourly_traffic": {
|
||||||
|
"date_histogram": {
|
||||||
|
"field": "@timestamp",
|
||||||
|
"calendar_interval": "hour"
|
||||||
|
},
|
||||||
|
"aggs": {
|
||||||
|
"bytes": {"sum": {"field": "bytes"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.es_client.search(
|
||||||
|
index=self.config['elasticsearch']['index'],
|
||||||
|
body=query
|
||||||
|
)
|
||||||
|
|
||||||
|
total_hits = result['hits']['total']
|
||||||
|
# Handle both ES 6.x and 7.x response formats
|
||||||
|
if isinstance(total_hits, dict):
|
||||||
|
total_count = total_hits['value']
|
||||||
|
else:
|
||||||
|
total_count = total_hits
|
||||||
|
|
||||||
|
logger.info(f"Collected traffic data: {total_count} flows")
|
||||||
|
return result['aggregations']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error collecting traffic data: {e}")
|
||||||
|
return self.generate_mock_data()
|
||||||
|
|
||||||
|
def generate_mock_data(self) -> Dict:
|
||||||
|
"""Generate mock traffic data for testing"""
|
||||||
|
return {
|
||||||
|
"top_talkers": {
|
||||||
|
"buckets": [
|
||||||
|
{"key": "192.168.100.50", "doc_count": 15000,
|
||||||
|
"bytes": {"value": 5000000}, "packets": {"value": 10000}},
|
||||||
|
{"key": "192.168.100.51", "doc_count": 12000,
|
||||||
|
"bytes": {"value": 4000000}, "packets": {"value": 8000}},
|
||||||
|
{"key": "192.168.100.11", "doc_count": 8000,
|
||||||
|
"bytes": {"value": 2000000}, "packets": {"value": 5000}},
|
||||||
|
{"key": "10.0.0.5", "doc_count": 6000,
|
||||||
|
"bytes": {"value": 1500000}, "packets": {"value": 3000}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"buckets": [
|
||||||
|
{"key": "tcp", "doc_count": 25000},
|
||||||
|
{"key": "udp", "doc_count": 15000},
|
||||||
|
{"key": "icmp", "doc_count": 2000}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vlans": {
|
||||||
|
"buckets": [
|
||||||
|
{"key": 100, "doc_count": 20000, "bytes": {"value": 8000000}},
|
||||||
|
{"key": 200, "doc_count": 15000, "bytes": {"value": 6000000}},
|
||||||
|
{"key": 300, "doc_count": 5000, "bytes": {"value": 2000000}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# def request_ai_analysis(self, traffic_data: Dict) -> Optional[Dict]:
|
||||||
|
# """Send traffic data to AI for analysis"""
|
||||||
|
# request_id = str(uuid.uuid4())
|
||||||
|
# request_file = self.request_dir / f"{request_id}.json"
|
||||||
|
#
|
||||||
|
# request_data = {
|
||||||
|
# "request_id": request_id,
|
||||||
|
# "timestamp": datetime.now().isoformat(),
|
||||||
|
# "type": "traffic_analysis",
|
||||||
|
# "data": traffic_data,
|
||||||
|
# "prompt": self.build_analysis_prompt(traffic_data)
|
||||||
|
# }
|
||||||
|
# try:
|
||||||
|
# with open(request_file, 'w') as f:
|
||||||
|
# json.dump(request_data, f, indent=2)
|
||||||
|
# logger.info(f"AI request created: {request_id}")
|
||||||
|
#
|
||||||
|
# # Wait for response (with timeout)
|
||||||
|
# response = self.wait_for_ai_response(request_id, timeout=600)
|
||||||
|
# return response
|
||||||
|
#
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Error requesting AI analysis: {e}")
|
||||||
|
# return None
|
||||||
|
def request_ai_analysis(self, traffic_data: Dict) -> Optional[Dict]:
|
||||||
|
"""Send traffic data to AI for analysis"""
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
|
request_file = self.request_dir / f"{request_id}.json"
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"request_id": request_id,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"type": "traffic_analysis",
|
||||||
|
"data": traffic_data,
|
||||||
|
"prompt": self.build_analysis_prompt(traffic_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(request_file, 'w') as f:
|
||||||
|
json.dump(request_data, f, indent=2)
|
||||||
|
logger.info(f"AI request created: {request_id}")
|
||||||
|
|
||||||
|
# Wait for response (with timeout)
|
||||||
|
response = self.wait_for_ai_response(request_id, timeout=600)
|
||||||
|
|
||||||
|
# Convert to pipeline format for PR creation
|
||||||
|
if response and 'response' in response:
|
||||||
|
# Create pipeline-compatible format
|
||||||
|
pipeline_response = {
|
||||||
|
"suggestions": [],
|
||||||
|
"focus_area": "security",
|
||||||
|
"feedback_aware": True,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract SRX config lines from AI response
|
||||||
|
ai_text = response.get('response', '')
|
||||||
|
lines = ai_text.split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
# Capture any line that looks like SRX config
|
||||||
|
if line.startswith('set '):
|
||||||
|
# Remove any trailing braces
|
||||||
|
clean_line = line.rstrip(' {')
|
||||||
|
clean_line = clean_line.rstrip('{')
|
||||||
|
pipeline_response['suggestions'].append({
|
||||||
|
"config": clean_line,
|
||||||
|
"reason": "AI-generated optimization"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only save if we found config lines
|
||||||
|
if pipeline_response['suggestions']:
|
||||||
|
pipeline_file = self.response_dir / f"pipeline_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{request_id[:8]}_response.json"
|
||||||
|
with open(pipeline_file, 'w') as f:
|
||||||
|
json.dump(pipeline_response, f, indent=2)
|
||||||
|
logger.info(f"Saved pipeline format response with {len(pipeline_response['suggestions'])} configs")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error requesting AI analysis: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_analysis_prompt(self, traffic_data: Dict) -> str:
|
||||||
|
"""Build prompt for AI analysis"""
|
||||||
|
prompt = """Analyze this network traffic data and suggest optimizations for a Juniper SRX firewall:
|
||||||
|
|
||||||
|
Traffic Summary:
|
||||||
|
- Top Talkers: {}
|
||||||
|
- Active VLANs: {}
|
||||||
|
- Protocol Distribution: {}
|
||||||
|
|
||||||
|
Based on this data, please provide:
|
||||||
|
1. Security rule optimizations (as Juniper SRX configuration commands)
|
||||||
|
2. QoS improvements for high-traffic hosts
|
||||||
|
3. VLAN segmentation recommendations
|
||||||
|
4. Potential security concerns or anomalies
|
||||||
|
|
||||||
|
Format your response with specific Juniper SRX configuration commands that can be applied.
|
||||||
|
Include comments explaining each change."""
|
||||||
|
|
||||||
|
# Extract key metrics
|
||||||
|
top_ips = [b['key'] for b in traffic_data.get('top_talkers', {}).get('buckets', [])][:5]
|
||||||
|
vlans = [str(b['key']) for b in traffic_data.get('vlans', {}).get('buckets', [])][:5]
|
||||||
|
protocols = [str(b['key']) for b in traffic_data.get('protocols', {}).get('buckets', [])][:3]
|
||||||
|
|
||||||
|
return prompt.format(
|
||||||
|
', '.join(top_ips) if top_ips else 'No data',
|
||||||
|
', '.join(vlans) if vlans else 'No VLANs',
|
||||||
|
', '.join(protocols) if protocols else 'No protocols'
|
||||||
|
)
|
||||||
|
|
||||||
|
def wait_for_ai_response(self, request_id: str, timeout: int = 120) -> Optional[Dict]:
|
||||||
|
"""Wait for AI response file"""
|
||||||
|
response_file = self.response_dir / f"{request_id}_response.json"
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
if response_file.exists():
|
||||||
|
try:
|
||||||
|
time.sleep(1) # Give AI time to finish writing
|
||||||
|
with open(response_file, 'r') as f:
|
||||||
|
response = json.load(f)
|
||||||
|
logger.info(f"AI response received: {request_id}")
|
||||||
|
|
||||||
|
# Log a snippet of the response
|
||||||
|
if 'response' in response:
|
||||||
|
snippet = response['response'][:200] + '...' if len(response['response']) > 200 else response['response']
|
||||||
|
logger.info(f"AI suggestion snippet: {snippet}")
|
||||||
|
|
||||||
|
# Clean up files
|
||||||
|
response_file.unlink()
|
||||||
|
(self.request_dir / f"{request_id}.json").unlink(missing_ok=True)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading AI response: {e}")
|
||||||
|
return None
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
logger.warning(f"AI response timeout for {request_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# REPLACE THE EXISTING create_gitea_pr METHOD WITH THIS ENHANCED VERSION
|
||||||
|
def create_gitea_pr(self, ai_response: Dict = None) -> bool:
|
||||||
|
"""Create pull request in Gitea with suggested changes"""
|
||||||
|
try:
|
||||||
|
# If no AI response provided, get the latest one
|
||||||
|
if not ai_response:
|
||||||
|
latest_suggestion = self._get_latest_ai_suggestion()
|
||||||
|
if not latest_suggestion:
|
||||||
|
logger.warning("No AI suggestions found to create PR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Read the suggestion file
|
||||||
|
with open(latest_suggestion['path'], 'r') as f:
|
||||||
|
ai_response = json.load(f)
|
||||||
|
|
||||||
|
# Check if we should create a PR
|
||||||
|
if not self.should_create_pr():
|
||||||
|
logger.info("Skipping PR creation - conditions not met")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for existing pending PR
|
||||||
|
if self.state.get('pending_pr'):
|
||||||
|
logger.info(f"Skipping PR creation - pending PR exists: {self.state['pending_pr']}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Creating Gitea pull request with AI suggestions...")
|
||||||
|
|
||||||
|
# Initialize Gitea integration
|
||||||
|
gitea = GiteaIntegration(self.config['gitea'])
|
||||||
|
|
||||||
|
# Extract the SRX configuration
|
||||||
|
srx_config = ai_response.get('suggestions', ai_response.get('response', ''))
|
||||||
|
if not srx_config or srx_config.strip() == '':
|
||||||
|
logger.warning("Empty or invalid suggestions, skipping PR creation")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create the PR
|
||||||
|
pr_info = gitea.create_pr_with_config(
|
||||||
|
srx_config=srx_config,
|
||||||
|
title=f"AI Network Configuration Suggestions - {datetime.now().strftime('%B %d, %Y')}",
|
||||||
|
description=None # Will auto-generate
|
||||||
|
)
|
||||||
|
|
||||||
|
if pr_info:
|
||||||
|
# Update state with PR information
|
||||||
|
self.state['pending_pr'] = pr_info['number']
|
||||||
|
self.state['last_pr_created'] = datetime.now().isoformat()
|
||||||
|
self.state['pr_url'] = pr_info['url']
|
||||||
|
self.save_state(self.state)
|
||||||
|
|
||||||
|
logger.info(f"Successfully created PR #{pr_info['number']}: {pr_info['url']}")
|
||||||
|
|
||||||
|
# Log to a separate file for notifications/monitoring
|
||||||
|
with open('/var/log/orchestrator/pr_created.log', 'a') as f:
|
||||||
|
f.write(f"{datetime.now().isoformat()} - Created PR #{pr_info['number']} - {pr_info['url']}\n")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Failed to create PR in Gitea")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating Gitea PR: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ADD THIS NEW METHOD
|
||||||
|
def _get_latest_ai_suggestion(self) -> Optional[Dict]:
|
||||||
|
"""Get the most recent AI suggestion file"""
|
||||||
|
response_dir = '/shared/ai-gitops/responses'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# List all response files
|
||||||
|
response_files = []
|
||||||
|
for filename in os.listdir(response_dir):
|
||||||
|
if filename.startswith('response_') and filename.endswith('.json'):
|
||||||
|
filepath = os.path.join(response_dir, filename)
|
||||||
|
# Get file modification time
|
||||||
|
mtime = os.path.getmtime(filepath)
|
||||||
|
response_files.append({
|
||||||
|
'path': filepath,
|
||||||
|
'filename': filename,
|
||||||
|
'mtime': mtime
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by modification time and get the latest
|
||||||
|
response_files.sort(key=lambda x: x['mtime'], reverse=True)
|
||||||
|
return response_files[0]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finding latest AI suggestion: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ADD THIS NEW METHOD
|
||||||
|
def check_pr_status(self):
|
||||||
|
"""Check the status of pending pull requests"""
|
||||||
|
if not self.state.get('pending_pr'):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
gitea = GiteaIntegration(self.config['gitea'])
|
||||||
|
pr_status = gitea.get_pr_status(self.state['pending_pr'])
|
||||||
|
|
||||||
|
if pr_status:
|
||||||
|
logger.info(f"PR #{pr_status['number']} status: {pr_status['state']} (merged: {pr_status['merged']})")
|
||||||
|
|
||||||
|
# If PR is closed or merged, clear the pending_pr flag
|
||||||
|
if pr_status['state'] == 'closed':
|
||||||
|
logger.info(f"PR #{pr_status['number']} has been closed")
|
||||||
|
self.state['pending_pr'] = None
|
||||||
|
self.state['last_pr_status'] = 'closed'
|
||||||
|
self.state['last_pr_closed'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
if pr_status['merged']:
|
||||||
|
self.state['last_pr_status'] = 'merged'
|
||||||
|
logger.info(f"PR #{pr_status['number']} was merged!")
|
||||||
|
# Mark for deployment
|
||||||
|
self.state['pending_deployment'] = True
|
||||||
|
self.state['deployment_pr'] = pr_status['number']
|
||||||
|
|
||||||
|
self.save_state(self.state)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking PR status: {e}")
|
||||||
|
|
||||||
|
def run_analysis_cycle(self):
|
||||||
|
"""Run a complete analysis cycle"""
|
||||||
|
logger.info("="*60)
|
||||||
|
logger.info("Starting traffic analysis cycle")
|
||||||
|
logger.info("="*60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Always collect traffic data
|
||||||
|
logger.info("Step 1: Collecting traffic data from Elasticsearch...")
|
||||||
|
traffic_data = self.collect_traffic_data()
|
||||||
|
|
||||||
|
if not traffic_data:
|
||||||
|
logger.warning("No traffic data available, skipping analysis")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Log summary of collected data
|
||||||
|
top_talkers = traffic_data.get('top_talkers', {}).get('buckets', [])
|
||||||
|
if top_talkers:
|
||||||
|
logger.info(f"Found {len(top_talkers)} top talkers")
|
||||||
|
logger.info(f"Top IP: {top_talkers[0]['key']} with {top_talkers[0]['doc_count']} flows")
|
||||||
|
|
||||||
|
# Check smart triggers if configured
|
||||||
|
trigger_fired = False
|
||||||
|
if self.config['pr_creation'].get('frequency') == 'smart':
|
||||||
|
trigger_fired = self.check_smart_triggers(traffic_data)
|
||||||
|
if trigger_fired:
|
||||||
|
logger.info("🎯 Smart trigger activated - will create PR")
|
||||||
|
|
||||||
|
# Always request AI analysis
|
||||||
|
logger.info("Step 2: Requesting AI analysis...")
|
||||||
|
ai_response = self.request_ai_analysis(traffic_data)
|
||||||
|
|
||||||
|
# Save state for analysis
|
||||||
|
self.save_state({
|
||||||
|
'last_analysis_run': datetime.now().isoformat(),
|
||||||
|
'last_analysis_data': {
|
||||||
|
'top_talkers_count': len(top_talkers),
|
||||||
|
'response_received': bool(ai_response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Determine if we should create PR
|
||||||
|
should_create = False
|
||||||
|
|
||||||
|
if self.config['pr_creation'].get('frequency') == 'smart':
|
||||||
|
# Smart mode: create if trigger fired
|
||||||
|
should_create = trigger_fired
|
||||||
|
else:
|
||||||
|
# Regular mode: use existing schedule logic
|
||||||
|
should_create = self.should_create_pr()
|
||||||
|
|
||||||
|
# Create PR if conditions met
|
||||||
|
if should_create:
|
||||||
|
if ai_response and (ai_response.get('response') or ai_response.get('suggestions')):
|
||||||
|
logger.info("Step 3: Creating PR with AI suggestions...")
|
||||||
|
if self.create_gitea_pr(ai_response):
|
||||||
|
logger.info("✓ PR created successfully")
|
||||||
|
if trigger_fired:
|
||||||
|
logger.info(" Reason: Smart trigger fired")
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to create Gitea PR")
|
||||||
|
else:
|
||||||
|
logger.info("No actionable suggestions from AI analysis")
|
||||||
|
else:
|
||||||
|
logger.info("No triggers met - analysis saved for future use")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in analysis cycle: {e}")
|
||||||
|
|
||||||
|
logger.info("="*60)
|
||||||
|
|
||||||
|
def main_loop(self):
|
||||||
|
"""Main orchestrator loop"""
|
||||||
|
logger.info("Starting Network AI Orchestrator")
|
||||||
|
|
||||||
|
# Setup Elasticsearch connection
|
||||||
|
if not self.setup_elasticsearch():
|
||||||
|
logger.warning("Running without Elasticsearch connection - using mock data")
|
||||||
|
|
||||||
|
interval = self.config['analysis'].get('interval_minutes', 60) * 60
|
||||||
|
|
||||||
|
# Run first analysis immediately
|
||||||
|
self.run_analysis_cycle()
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
logger.info(f"Next analysis scheduled in {interval/60} minutes")
|
||||||
|
logger.info(f"Next run at: {(datetime.now() + timedelta(seconds=interval)).strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
|
# MODIFIED: Check PR status every 15 minutes during the wait
|
||||||
|
for i in range(int(interval / 60)): # Check every minute
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
# Check PR status every 15 minutes
|
||||||
|
if i % 15 == 14 and self.state.get('pending_pr'):
|
||||||
|
logger.info("Checking PR status...")
|
||||||
|
self.check_pr_status()
|
||||||
|
|
||||||
|
if self.running: # Check again after sleep
|
||||||
|
self.run_analysis_cycle()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Received keyboard interrupt")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in main loop: {e}")
|
||||||
|
time.sleep(60) # Wait before retry
|
||||||
|
|
||||||
|
logger.info("Orchestrator shutdown complete")
|
||||||
|
|
||||||
|
def shutdown(self, signum=None, frame=None):
|
||||||
|
"""Graceful shutdown handler"""
|
||||||
|
if signum:
|
||||||
|
logger.info(f"Received signal {signum}, initiating shutdown...")
|
||||||
|
else:
|
||||||
|
logger.info("Initiating shutdown...")
|
||||||
|
self.running = False
|
||||||
|
if self.es_client:
|
||||||
|
try:
|
||||||
|
# Close Elasticsearch connection
|
||||||
|
self.es_client.transport.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
orchestrator = NetworkOrchestrator()
|
||||||
|
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGTERM, orchestrator.shutdown)
|
||||||
|
signal.signal(signal.SIGINT, orchestrator.shutdown)
|
||||||
|
|
||||||
|
try:
|
||||||
|
orchestrator.main_loop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fatal error: {e}", exc_info=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
30
scripts/orchestrator/core/requirements.txt
Normal file
30
scripts/orchestrator/core/requirements.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
aiofiles==24.1.0
|
||||||
|
bcrypt==4.3.0
|
||||||
|
blinker==1.9.0
|
||||||
|
certifi==2025.8.3
|
||||||
|
cffi==1.17.1
|
||||||
|
charset-normalizer==3.4.2
|
||||||
|
click==8.2.1
|
||||||
|
cryptography==45.0.5
|
||||||
|
elastic-transport==9.1.0
|
||||||
|
elasticsearch==7.17.9
|
||||||
|
Flask==3.1.1
|
||||||
|
gitdb==4.0.12
|
||||||
|
GitPython==3.1.45
|
||||||
|
idna==3.10
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
paramiko==3.5.1
|
||||||
|
pycparser==2.22
|
||||||
|
PyNaCl==1.5.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-dotenv==1.1.1
|
||||||
|
PyYAML==6.0.2
|
||||||
|
requests==2.32.4
|
||||||
|
six==1.17.0
|
||||||
|
smmap==5.0.2
|
||||||
|
tabulate==0.9.0
|
||||||
|
typing_extensions==4.14.1
|
||||||
|
urllib3==1.26.20
|
||||||
|
Werkzeug==3.1.3
|
||||||
141
scripts/orchestrator/gitea/close_pr_with_feedback.py
Executable file
141
scripts/orchestrator/gitea/close_pr_with_feedback.py
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Close PR with Feedback - Reject a PR and help AI learn
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
sys.path.append('/home/netops/orchestrator')
|
||||||
|
from gitea_integration import GiteaIntegration
|
||||||
|
from pr_feedback import PRFeedbackSystem
|
||||||
|
|
||||||
|
def close_pr_with_feedback(pr_number: int, reason: str, issues: List[str]):
|
||||||
|
"""Close a PR and record feedback for AI learning"""
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
with open('/home/netops/orchestrator/config.yaml', 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Initialize systems
|
||||||
|
gitea = GiteaIntegration(config['gitea'])
|
||||||
|
feedback_system = PRFeedbackSystem()
|
||||||
|
|
||||||
|
print(f"\n🚫 Closing PR #{pr_number} with feedback...")
|
||||||
|
|
||||||
|
# First, add a comment to the PR explaining why it's being closed
|
||||||
|
comment = f"""## 🤖 AI Configuration Review - Rejected
|
||||||
|
|
||||||
|
**Reason**: {reason}
|
||||||
|
|
||||||
|
**Issues Found**:
|
||||||
|
{chr(10).join(f'- {issue}' for issue in issues)}
|
||||||
|
|
||||||
|
This feedback has been recorded to improve future AI suggestions. The AI will learn from these issues and avoid them in future configurations.
|
||||||
|
|
||||||
|
### Specific Problems:
|
||||||
|
- **Security**: The any/any/any permit rule is too permissive
|
||||||
|
- **Best Practice**: Source addresses should be specific, not 'any'
|
||||||
|
- **Risk**: This configuration could expose the network to threats
|
||||||
|
|
||||||
|
The AI will generate better suggestions next time based on this feedback.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Post comment to PR (using Gitea API)
|
||||||
|
api_url = f"{config['gitea']['url']}/api/v1/repos/{config['gitea']['repo']}/issues/{pr_number}/comments"
|
||||||
|
headers = {
|
||||||
|
'Authorization': f"token {config['gitea']['token']}",
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
comment_data = {"body": comment}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(api_url, json=comment_data, headers=headers)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print("✅ Added feedback comment to PR")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Could not add comment: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error adding comment: {e}")
|
||||||
|
|
||||||
|
# Record feedback for AI learning
|
||||||
|
feedback_details = {
|
||||||
|
'reason': reason,
|
||||||
|
'specific_issues': '\n'.join(issues),
|
||||||
|
'configuration_issues': [
|
||||||
|
{'type': 'security_permissive', 'description': 'Rules too permissive (any/any/any)'},
|
||||||
|
{'type': 'security_missing', 'description': 'Missing source address restrictions'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback_system.record_pr_feedback(pr_number, 'rejected', feedback_details)
|
||||||
|
|
||||||
|
# Update orchestrator state
|
||||||
|
state_file = '/var/lib/orchestrator/state.json'
|
||||||
|
try:
|
||||||
|
with open(state_file, 'r') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
state['pending_pr'] = None
|
||||||
|
state['last_pr_status'] = 'rejected'
|
||||||
|
state['last_pr_rejected'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
with open(state_file, 'w') as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
print("✅ Updated orchestrator state")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Could not update state: {e}")
|
||||||
|
|
||||||
|
# Show AI learning summary
|
||||||
|
patterns = feedback_system.analyze_feedback_patterns()
|
||||||
|
print(f"\n📊 AI Learning Summary:")
|
||||||
|
print(f"Total feedback entries: {patterns['total_prs']}")
|
||||||
|
print(f"Rejected PRs: {patterns['rejected']}")
|
||||||
|
print(f"Security concerns: {patterns['security_concerns']}")
|
||||||
|
|
||||||
|
print("\n✅ PR closed with feedback. The AI will learn from this!")
|
||||||
|
print("\nNext time the AI generates a configuration, it will:")
|
||||||
|
print("- Avoid any/any/any permit rules")
|
||||||
|
print("- Use specific source addresses")
|
||||||
|
print("- Follow security best practices")
|
||||||
|
|
||||||
|
print("\n⚠️ IMPORTANT: Now manually close the PR in Gitea!")
|
||||||
|
print(f"Go to: {config['gitea']['url']}/{config['gitea']['repo']}/pulls/{pr_number}")
|
||||||
|
print("Click the 'Close Pull Request' button")
|
||||||
|
|
||||||
|
# Quick reject function for current PR
|
||||||
|
def reject_current_pr():
|
||||||
|
"""Reject PR #2 with specific feedback"""
|
||||||
|
close_pr_with_feedback(
|
||||||
|
pr_number=2,
|
||||||
|
reason="Security policy too permissive - any/any/any permit rule is dangerous",
|
||||||
|
issues=[
|
||||||
|
"ALLOW-ESTABLISHED policy permits all traffic from trust to untrust",
|
||||||
|
"Source address should not be 'any' - use specific networks",
|
||||||
|
"Application should not be 'any' - specify required services only",
|
||||||
|
"This configuration could expose internal network to threats"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--current":
|
||||||
|
# Reject the current PR #2
|
||||||
|
reject_current_pr()
|
||||||
|
else:
|
||||||
|
# Interactive mode
|
||||||
|
pr_num = input("Enter PR number to reject: ")
|
||||||
|
reason = input("Reason for rejection: ")
|
||||||
|
issues = []
|
||||||
|
print("Enter specific issues (empty line to finish):")
|
||||||
|
while True:
|
||||||
|
issue = input("- ")
|
||||||
|
if not issue:
|
||||||
|
break
|
||||||
|
issues.append(issue)
|
||||||
|
|
||||||
|
close_pr_with_feedback(int(pr_num), reason, issues)
|
||||||
377
scripts/orchestrator/gitea/gitea_pr_feedback.py
Executable file
377
scripts/orchestrator/gitea/gitea_pr_feedback.py
Executable file
@@ -0,0 +1,377 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Gitea PR Creation and Feedback Handler
|
||||||
|
Creates real PRs in Gitea and handles rejection feedback
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
class GiteaPRManager:
|
||||||
|
def __init__(self, config_path='/home/netops/orchestrator/config.yaml'):
|
||||||
|
"""Initialize with Gitea configuration"""
|
||||||
|
# Load config
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
self.gitea_config = self.config.get('gitea', {})
|
||||||
|
self.base_url = self.gitea_config.get('url', 'http://localhost:3000')
|
||||||
|
self.token = self.gitea_config.get('token', '')
|
||||||
|
self.repo_owner = self.gitea_config.get('owner', 'netops')
|
||||||
|
self.repo_name = self.gitea_config.get('repo', 'srx-config')
|
||||||
|
|
||||||
|
self.headers = {
|
||||||
|
'Authorization': f'token {self.token}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pending_prs_dir = Path('/shared/ai-gitops/pending_prs')
|
||||||
|
self.feedback_dir = Path('/shared/ai-gitops/feedback')
|
||||||
|
|
||||||
|
def create_pr_from_ai_suggestions(self, pr_file=None):
|
||||||
|
"""Create a PR in Gitea from AI suggestions"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("Creating Gitea PR from AI Suggestions")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Find latest PR file if not specified
|
||||||
|
if pr_file is None:
|
||||||
|
pr_files = sorted(self.pending_prs_dir.glob('pr_*.json'),
|
||||||
|
key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
|
if not pr_files:
|
||||||
|
print("❌ No pending PR files found")
|
||||||
|
return None
|
||||||
|
pr_file = pr_files[0]
|
||||||
|
|
||||||
|
print(f"📄 Using PR file: {pr_file.name}")
|
||||||
|
|
||||||
|
# Load PR data
|
||||||
|
with open(pr_file, 'r') as f:
|
||||||
|
pr_data = json.load(f)
|
||||||
|
|
||||||
|
# Create a new branch
|
||||||
|
branch_name = f"ai-suggestions-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||||||
|
|
||||||
|
# Create the configuration file content
|
||||||
|
config_content = f"""# AI-Generated Network Configuration
|
||||||
|
# Generated: {pr_data.get('timestamp', datetime.now().isoformat())}
|
||||||
|
# Model: {pr_data.get('model', 'llama2:13b')}
|
||||||
|
# Feedback Aware: {pr_data.get('feedback_aware', False)}
|
||||||
|
|
||||||
|
{pr_data.get('suggestions', '')}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create branch and file via Gitea API
|
||||||
|
try:
|
||||||
|
# First, get the default branch SHA
|
||||||
|
repo_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}"
|
||||||
|
repo_response = requests.get(repo_url, headers=self.headers)
|
||||||
|
|
||||||
|
if repo_response.status_code != 200:
|
||||||
|
print(f"❌ Failed to get repo info: {repo_response.status_code}")
|
||||||
|
print(f" Response: {repo_response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
default_branch = repo_response.json().get('default_branch', 'main')
|
||||||
|
|
||||||
|
# Get the SHA of the default branch
|
||||||
|
branch_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/branches/{default_branch}"
|
||||||
|
branch_response = requests.get(branch_url, headers=self.headers)
|
||||||
|
|
||||||
|
if branch_response.status_code != 200:
|
||||||
|
print(f"❌ Failed to get branch info: {branch_response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
base_sha = branch_response.json()['commit']['id']
|
||||||
|
|
||||||
|
# Create new branch
|
||||||
|
create_branch_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/branches"
|
||||||
|
branch_data = {
|
||||||
|
'new_branch_name': branch_name,
|
||||||
|
'old_branch_name': default_branch
|
||||||
|
}
|
||||||
|
|
||||||
|
branch_create = requests.post(create_branch_url,
|
||||||
|
headers=self.headers,
|
||||||
|
json=branch_data)
|
||||||
|
|
||||||
|
if branch_create.status_code not in [201, 200]:
|
||||||
|
print(f"❌ Failed to create branch: {branch_create.status_code}")
|
||||||
|
print(f" Response: {branch_create.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"✅ Created branch: {branch_name}")
|
||||||
|
|
||||||
|
# Create or update file in the new branch
|
||||||
|
file_path = f"ai-suggestions/config_{datetime.now().strftime('%Y%m%d_%H%M%S')}.conf"
|
||||||
|
file_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/contents/{file_path}"
|
||||||
|
|
||||||
|
import base64
|
||||||
|
file_data = {
|
||||||
|
'branch': branch_name,
|
||||||
|
'content': base64.b64encode(config_content.encode()).decode(),
|
||||||
|
'message': f"AI suggestions: {pr_data.get('title', 'Network optimization')}"
|
||||||
|
}
|
||||||
|
|
||||||
|
file_response = requests.post(file_url, headers=self.headers, json=file_data)
|
||||||
|
|
||||||
|
if file_response.status_code not in [201, 200]:
|
||||||
|
print(f"⚠️ Could not create file via API, trying alternative method")
|
||||||
|
else:
|
||||||
|
print(f"✅ Created config file: {file_path}")
|
||||||
|
|
||||||
|
# Create Pull Request
|
||||||
|
pr_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/pulls"
|
||||||
|
|
||||||
|
pr_body = f"""## AI-Generated Network Configuration
|
||||||
|
|
||||||
|
### Analysis Context
|
||||||
|
- **Zones Analyzed**: {', '.join(pr_data.get('network_context', {}).get('zones', []))}
|
||||||
|
- **Policies Reviewed**: {pr_data.get('network_context', {}).get('policies', 0)}
|
||||||
|
- **Feedback Aware**: {pr_data.get('feedback_aware', False)}
|
||||||
|
|
||||||
|
### Suggested Changes
|
||||||
|
```junos
|
||||||
|
{pr_data.get('suggestions', '')}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Checklist
|
||||||
|
- [ ] No any/any/any rules
|
||||||
|
- [ ] Logging enabled on all policies
|
||||||
|
- [ ] Proper zone segmentation
|
||||||
|
- [ ] Address-sets used instead of individual IPs
|
||||||
|
- [ ] Applications are specific (not "any")
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Apply to lab SRX first
|
||||||
|
2. Verify traffic flow
|
||||||
|
3. Check logs for any issues
|
||||||
|
4. Apply to production if tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
*This PR was automatically generated by the AI Network Automation system*
|
||||||
|
"""
|
||||||
|
|
||||||
|
pr_request = {
|
||||||
|
'title': pr_data.get('title', 'AI Network Configuration Suggestions'),
|
||||||
|
'head': branch_name,
|
||||||
|
'base': default_branch,
|
||||||
|
'body': pr_body
|
||||||
|
}
|
||||||
|
|
||||||
|
pr_response = requests.post(pr_url, headers=self.headers, json=pr_request)
|
||||||
|
|
||||||
|
if pr_response.status_code == 201:
|
||||||
|
pr_info = pr_response.json()
|
||||||
|
pr_number = pr_info['number']
|
||||||
|
pr_html_url = pr_info['html_url']
|
||||||
|
|
||||||
|
print(f"\n✅ Pull Request created successfully!")
|
||||||
|
print(f" PR Number: #{pr_number}")
|
||||||
|
print(f" URL: {pr_html_url}")
|
||||||
|
|
||||||
|
# Save PR info for tracking
|
||||||
|
pr_tracking = {
|
||||||
|
'pr_number': pr_number,
|
||||||
|
'pr_url': pr_html_url,
|
||||||
|
'branch': branch_name,
|
||||||
|
'created_at': datetime.now().isoformat(),
|
||||||
|
'ai_request_id': pr_data.get('request_id'),
|
||||||
|
'suggestions_file': str(pr_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
tracking_file = self.pending_prs_dir / f"gitea_pr_{pr_number}.json"
|
||||||
|
with open(tracking_file, 'w') as f:
|
||||||
|
json.dump(pr_tracking, f, indent=2)
|
||||||
|
|
||||||
|
return pr_number
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to create PR: {pr_response.status_code}")
|
||||||
|
print(f" Response: {pr_response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating PR: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def reject_pr_with_feedback(self, pr_number, feedback_message):
|
||||||
|
"""Reject a PR and save feedback for AI learning"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"Rejecting PR #{pr_number} with Feedback")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Close the PR via API
|
||||||
|
pr_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/pulls/{pr_number}"
|
||||||
|
|
||||||
|
# Add comment with feedback
|
||||||
|
comment_url = f"{pr_url}/reviews"
|
||||||
|
comment_data = {
|
||||||
|
'body': feedback_message,
|
||||||
|
'event': 'REJECT' # or 'REQUEST_CHANGES'
|
||||||
|
}
|
||||||
|
|
||||||
|
comment_response = requests.post(comment_url, headers=self.headers, json=comment_data)
|
||||||
|
|
||||||
|
if comment_response.status_code not in [200, 201]:
|
||||||
|
# Try alternative: just add a comment
|
||||||
|
issue_comment_url = f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/issues/{pr_number}/comments"
|
||||||
|
comment_data = {
|
||||||
|
'body': f"❌ **REJECTED**\n\n{feedback_message}"
|
||||||
|
}
|
||||||
|
requests.post(issue_comment_url, headers=self.headers, json=comment_data)
|
||||||
|
|
||||||
|
# Close the PR
|
||||||
|
close_data = {
|
||||||
|
'state': 'closed'
|
||||||
|
}
|
||||||
|
close_response = requests.patch(pr_url, headers=self.headers, json=close_data)
|
||||||
|
|
||||||
|
if close_response.status_code == 200:
|
||||||
|
print(f"✅ PR #{pr_number} closed")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Could not close PR via API")
|
||||||
|
|
||||||
|
# Save feedback for AI learning
|
||||||
|
feedback_entry = {
|
||||||
|
'pr_number': pr_number,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'feedback_type': 'rejected',
|
||||||
|
'reviewer': 'security_team',
|
||||||
|
'details': {
|
||||||
|
'reason': feedback_message,
|
||||||
|
'specific_issues': self.parse_feedback_for_issues(feedback_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load and update feedback history
|
||||||
|
feedback_file = self.feedback_dir / 'pr_feedback_history.json'
|
||||||
|
self.feedback_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if feedback_file.exists():
|
||||||
|
with open(feedback_file, 'r') as f:
|
||||||
|
history = json.load(f)
|
||||||
|
else:
|
||||||
|
history = []
|
||||||
|
|
||||||
|
history.append(feedback_entry)
|
||||||
|
|
||||||
|
with open(feedback_file, 'w') as f:
|
||||||
|
json.dump(history, f, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Feedback saved for AI learning")
|
||||||
|
print(f" Total feedback entries: {len(history)}")
|
||||||
|
|
||||||
|
return feedback_entry
|
||||||
|
|
||||||
|
def parse_feedback_for_issues(self, feedback_text):
|
||||||
|
"""Parse feedback text to extract specific issues"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Common security issues to look for
|
||||||
|
patterns = [
|
||||||
|
('any/any/any', 'Never use any/any/any rules'),
|
||||||
|
('no logging', 'Always enable logging'),
|
||||||
|
('source-address any', 'Avoid using source-address any'),
|
||||||
|
('destination-address any', 'Avoid using destination-address any'),
|
||||||
|
('application any', 'Specify applications instead of any'),
|
||||||
|
('overly permissive', 'Rules are too permissive'),
|
||||||
|
('zone segmentation', 'Improper zone segmentation'),
|
||||||
|
('iot', 'IoT security concerns')
|
||||||
|
]
|
||||||
|
|
||||||
|
feedback_lower = feedback_text.lower()
|
||||||
|
for pattern, description in patterns:
|
||||||
|
if pattern in feedback_lower:
|
||||||
|
issues.append({
|
||||||
|
'pattern': pattern,
|
||||||
|
'description': description,
|
||||||
|
'type': 'security'
|
||||||
|
})
|
||||||
|
|
||||||
|
return issues if issues else feedback_text
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for testing"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(" GITEA PR FEEDBACK TESTING")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
manager = GiteaPRManager()
|
||||||
|
|
||||||
|
print("\nOptions:")
|
||||||
|
print("1. Create a new PR from latest AI suggestions")
|
||||||
|
print("2. Reject a PR with feedback")
|
||||||
|
print("3. Run complete test cycle")
|
||||||
|
|
||||||
|
choice = input("\nSelect option (1-3): ")
|
||||||
|
|
||||||
|
if choice == '1':
|
||||||
|
pr_number = manager.create_pr_from_ai_suggestions()
|
||||||
|
if pr_number:
|
||||||
|
print(f"\n✅ Successfully created PR #{pr_number}")
|
||||||
|
print("\nYou can now:")
|
||||||
|
print(f"1. Review it in Gitea")
|
||||||
|
print(f"2. Reject it with: python3 gitea_pr_feedback.py")
|
||||||
|
|
||||||
|
elif choice == '2':
|
||||||
|
pr_number = input("Enter PR number to reject: ")
|
||||||
|
print("\nEnter rejection feedback (press Ctrl+D when done):")
|
||||||
|
feedback_lines = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
feedback_lines.append(input())
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
feedback = '\n'.join(feedback_lines)
|
||||||
|
|
||||||
|
if not feedback:
|
||||||
|
feedback = """This configuration has security issues:
|
||||||
|
|
||||||
|
1. Any/any/any rules detected - this violates zero-trust principles
|
||||||
|
2. No logging enabled on some policies
|
||||||
|
3. Overly permissive access between zones
|
||||||
|
|
||||||
|
Please revise to:
|
||||||
|
- Use specific address-sets
|
||||||
|
- Enable logging on all policies
|
||||||
|
- Implement proper zone segmentation"""
|
||||||
|
|
||||||
|
manager.reject_pr_with_feedback(pr_number, feedback)
|
||||||
|
|
||||||
|
elif choice == '3':
|
||||||
|
print("\n📋 Complete test cycle:")
|
||||||
|
print("1. Creating PR from AI suggestions...")
|
||||||
|
pr_number = manager.create_pr_from_ai_suggestions()
|
||||||
|
|
||||||
|
if pr_number:
|
||||||
|
print(f"\n2. Waiting for review...")
|
||||||
|
input(" Press Enter to simulate rejection...")
|
||||||
|
|
||||||
|
feedback = """Security Review Failed:
|
||||||
|
|
||||||
|
❌ Critical Issues Found:
|
||||||
|
- Any/any/any rule in policy ALLOW-ALL
|
||||||
|
- No logging on DMZ policies
|
||||||
|
- IoT zone has unrestricted access to HOME zone
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- All policies must use specific addresses
|
||||||
|
- Logging must be enabled
|
||||||
|
- IoT devices need strict access control
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("\n3. Rejecting PR with feedback...")
|
||||||
|
manager.reject_pr_with_feedback(pr_number, feedback)
|
||||||
|
|
||||||
|
print("\n4. AI will learn from this feedback in next run")
|
||||||
|
print(" Run: python3 run_pipeline.py --skip-netflow")
|
||||||
|
print(" The AI should avoid these mistakes next time!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
316
scripts/orchestrator/gitea/webhook_listener.py
Executable file
316
scripts/orchestrator/gitea/webhook_listener.py
Executable file
@@ -0,0 +1,316 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Gitea Webhook Listener - Automatically captures PR approvals/rejections
|
||||||
|
Runs on orchestrator VM to capture feedback in real-time
|
||||||
|
"""
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# Load environment variables from home directory
|
||||||
|
env_path = Path.home() / '.env'
|
||||||
|
load_dotenv(env_path)
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
# This loads from .env file
|
||||||
|
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', '')
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('/var/log/webhook-listener.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
FEEDBACK_FILE = "/shared/ai-gitops/feedback/pr_feedback_history.json"
|
||||||
|
LEARNING_FILE = "/shared/ai-gitops/learning/patterns.json"
|
||||||
|
|
||||||
|
def ensure_directories():
|
||||||
|
"""Ensure required directories exist"""
|
||||||
|
Path(FEEDBACK_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
Path(LEARNING_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Ensure deployment log directory exists
|
||||||
|
Path('/var/log/orchestrator').mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def load_feedback_history():
|
||||||
|
"""Load existing feedback history"""
|
||||||
|
if Path(FEEDBACK_FILE).exists():
|
||||||
|
try:
|
||||||
|
with open(FEEDBACK_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_feedback_history(feedback):
|
||||||
|
"""Save updated feedback history"""
|
||||||
|
with open(FEEDBACK_FILE, 'w') as f:
|
||||||
|
json.dump(feedback, f, indent=2)
|
||||||
|
logger.info(f"Saved feedback history with {len(feedback)} entries")
|
||||||
|
|
||||||
|
def load_learning_patterns():
|
||||||
|
"""Load learning patterns"""
|
||||||
|
if Path(LEARNING_FILE).exists():
|
||||||
|
try:
|
||||||
|
with open(LEARNING_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {"avoid_patterns": [], "successful_patterns": []}
|
||||||
|
|
||||||
|
def save_learning_patterns(patterns):
|
||||||
|
"""Save learning patterns"""
|
||||||
|
with open(LEARNING_FILE, 'w') as f:
|
||||||
|
json.dump(patterns, f, indent=2)
|
||||||
|
logger.info("Updated learning patterns")
|
||||||
|
|
||||||
|
def extract_config_changes(pr_body):
|
||||||
|
"""Extract SRX config commands from PR body"""
|
||||||
|
if not pr_body:
|
||||||
|
return []
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
lines = pr_body.split('\n')
|
||||||
|
in_code_block = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('```'):
|
||||||
|
in_code_block = not in_code_block
|
||||||
|
elif in_code_block and line.startswith('set '):
|
||||||
|
configs.append(line)
|
||||||
|
elif not in_code_block and line.startswith('set '):
|
||||||
|
configs.append(line)
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def update_learning(feedback_entry):
|
||||||
|
"""Update AI learning patterns based on feedback"""
|
||||||
|
patterns = load_learning_patterns()
|
||||||
|
|
||||||
|
if feedback_entry["status"] == "rejected":
|
||||||
|
# Add rejected patterns
|
||||||
|
for config in feedback_entry.get("config_changes", []):
|
||||||
|
if config not in patterns["avoid_patterns"]:
|
||||||
|
patterns["avoid_patterns"].append(config)
|
||||||
|
|
||||||
|
# Mark common rejection reasons
|
||||||
|
reason = feedback_entry.get("reason", "").lower()
|
||||||
|
if "any any any" in reason or "any/any/any" in reason:
|
||||||
|
patterns["avoid_patterns"].append("any-any-any-pattern")
|
||||||
|
if "redundant" in reason or "already configured" in reason:
|
||||||
|
patterns["avoid_patterns"].append("redundant-config")
|
||||||
|
if "too broad" in reason or "overly permissive" in reason:
|
||||||
|
patterns["avoid_patterns"].append("overly-permissive")
|
||||||
|
|
||||||
|
elif feedback_entry["status"] == "approved":
|
||||||
|
# Track successful patterns
|
||||||
|
for config in feedback_entry.get("config_changes", []):
|
||||||
|
if config not in patterns["successful_patterns"]:
|
||||||
|
patterns["successful_patterns"].append(config)
|
||||||
|
|
||||||
|
save_learning_patterns(patterns)
|
||||||
|
logger.info(f"Learning updated: {len(patterns['avoid_patterns'])} patterns to avoid")
|
||||||
|
|
||||||
|
@app.route('/webhook', methods=['POST'])
|
||||||
|
def handle_webhook():
|
||||||
|
"""Main webhook handler for Gitea PR events"""
|
||||||
|
try:
|
||||||
|
# Verify webhook signature for security
|
||||||
|
if WEBHOOK_SECRET:
|
||||||
|
signature = request.headers.get('X-Gitea-Signature', '')
|
||||||
|
if not signature:
|
||||||
|
logger.warning("No signature provided in webhook request")
|
||||||
|
return jsonify({"error": "No signature"}), 403
|
||||||
|
|
||||||
|
# Calculate expected signature
|
||||||
|
expected = 'sha256=' + hmac.new(
|
||||||
|
WEBHOOK_SECRET.encode(),
|
||||||
|
request.data,
|
||||||
|
hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# Compare signatures
|
||||||
|
if not hmac.compare_digest(signature, expected):
|
||||||
|
logger.warning(f"Invalid signature from {request.remote_addr}")
|
||||||
|
return jsonify({"error": "Invalid signature"}), 403
|
||||||
|
|
||||||
|
logger.debug("Webhook signature verified successfully")
|
||||||
|
|
||||||
|
# Get event data
|
||||||
|
data = request.json
|
||||||
|
event = request.headers.get('X-Gitea-Event', '')
|
||||||
|
|
||||||
|
logger.info(f"Received event: {event}")
|
||||||
|
|
||||||
|
if event != "pull_request":
|
||||||
|
return jsonify({"status": "ignored", "reason": "Not a PR event"}), 200
|
||||||
|
|
||||||
|
action = data.get('action', '')
|
||||||
|
pr = data.get('pull_request', {})
|
||||||
|
|
||||||
|
# Check if this is an AI-generated PR
|
||||||
|
pr_title = pr.get('title', '')
|
||||||
|
if 'AI-Generated' not in pr_title and 'Network Configuration Update' not in pr_title:
|
||||||
|
logger.info(f"Ignoring non-AI PR: {pr_title}")
|
||||||
|
return jsonify({"status": "ignored", "reason": "Not AI-generated"}), 200
|
||||||
|
|
||||||
|
# Process closed PRs (either merged or rejected)
|
||||||
|
if action == "closed":
|
||||||
|
pr_number = pr.get('number', 0)
|
||||||
|
pr_body = pr.get('body', '')
|
||||||
|
merged = pr.get('merged', False)
|
||||||
|
|
||||||
|
# Extract config changes from PR body
|
||||||
|
config_changes = extract_config_changes(pr_body)
|
||||||
|
|
||||||
|
# Create feedback entry
|
||||||
|
feedback_entry = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"pr_number": pr_number,
|
||||||
|
"pr_title": pr_title,
|
||||||
|
"status": "approved" if merged else "rejected",
|
||||||
|
"config_changes": config_changes,
|
||||||
|
"merged": merged
|
||||||
|
}
|
||||||
|
|
||||||
|
# For rejected PRs, try to extract reason from PR comments or description
|
||||||
|
if not merged:
|
||||||
|
feedback_entry["feedback_type"] = "rejected" # For compatibility
|
||||||
|
# Look for common rejection patterns in title or last comment
|
||||||
|
if "any" in str(config_changes).lower():
|
||||||
|
feedback_entry["reason"] = "Contains any/any/any patterns"
|
||||||
|
else:
|
||||||
|
feedback_entry["reason"] = "Changes not needed or incorrect"
|
||||||
|
|
||||||
|
logger.info(f"❌ PR #{pr_number} REJECTED - {pr_title}")
|
||||||
|
else:
|
||||||
|
feedback_entry["feedback_type"] = "approved" # For compatibility
|
||||||
|
logger.info(f"✅ PR #{pr_number} APPROVED - {pr_title}")
|
||||||
|
|
||||||
|
# Save feedback
|
||||||
|
feedback = load_feedback_history()
|
||||||
|
feedback.append(feedback_entry)
|
||||||
|
save_feedback_history(feedback)
|
||||||
|
|
||||||
|
# Update learning patterns
|
||||||
|
update_learning(feedback_entry)
|
||||||
|
|
||||||
|
# AUTO-DEPLOYMENT CODE - If PR was merged, trigger deployment
|
||||||
|
if merged:
|
||||||
|
logger.info(f"PR #{pr_number} was merged - triggering auto-deployment")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
'/home/netops/orchestrator/venv/bin/python',
|
||||||
|
'/home/netops/orchestrator/deploy_approved.py'
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"✅ Successfully auto-deployed PR #{pr_number}")
|
||||||
|
# Log deployment
|
||||||
|
with open('/var/log/orchestrator/deployments.log', 'a') as f:
|
||||||
|
f.write(f"{datetime.now().isoformat()} - Auto-deployed PR #{pr_number}\n")
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ Auto-deployment failed: {result.stderr}")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Deployment timed out after 5 minutes")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Deployment error: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "recorded",
|
||||||
|
"pr_number": pr_number,
|
||||||
|
"decision": feedback_entry["status"],
|
||||||
|
"configs_captured": len(config_changes),
|
||||||
|
"deployed": merged # Indicate if deployment was triggered
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
return jsonify({"status": "ignored", "reason": f"Action {action} not processed"}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing webhook: {e}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return jsonify({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "webhook-listener",
|
||||||
|
"feedback_file": str(Path(FEEDBACK_FILE).exists()),
|
||||||
|
"learning_file": str(Path(LEARNING_FILE).exists())
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
@app.route('/stats', methods=['GET'])
|
||||||
|
def get_stats():
|
||||||
|
"""Get feedback statistics"""
|
||||||
|
try:
|
||||||
|
feedback = load_feedback_history()
|
||||||
|
patterns = load_learning_patterns()
|
||||||
|
|
||||||
|
approved = len([f for f in feedback if f.get("status") == "approved"])
|
||||||
|
rejected = len([f for f in feedback if f.get("status") == "rejected"])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"total_prs": len(feedback),
|
||||||
|
"approved": approved,
|
||||||
|
"rejected": rejected,
|
||||||
|
"approval_rate": f"{(approved/len(feedback)*100):.1f}%" if feedback else "0%",
|
||||||
|
"patterns_to_avoid": len(patterns.get("avoid_patterns", [])),
|
||||||
|
"successful_patterns": len(patterns.get("successful_patterns", [])),
|
||||||
|
"last_feedback": feedback[-1]["timestamp"] if feedback else None
|
||||||
|
}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/feedback/recent', methods=['GET'])
|
||||||
|
def recent_feedback():
|
||||||
|
"""Get recent feedback entries"""
|
||||||
|
try:
|
||||||
|
feedback = load_feedback_history()
|
||||||
|
recent = feedback[-5:] if len(feedback) > 5 else feedback
|
||||||
|
recent.reverse() # Newest first
|
||||||
|
return jsonify(recent), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/learning/patterns', methods=['GET'])
|
||||||
|
def get_patterns():
|
||||||
|
"""Get current learning patterns"""
|
||||||
|
try:
|
||||||
|
patterns = load_learning_patterns()
|
||||||
|
return jsonify(patterns), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Ensure directories exist
|
||||||
|
ensure_directories()
|
||||||
|
|
||||||
|
logger.info("Starting Gitea webhook listener...")
|
||||||
|
logger.info(f"Feedback file: {FEEDBACK_FILE}")
|
||||||
|
logger.info(f"Learning file: {LEARNING_FILE}")
|
||||||
|
|
||||||
|
# Run Flask app
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
160
scripts/orchestrator/pipeline/create_ai_pr.py
Executable file
160
scripts/orchestrator/pipeline/create_ai_pr.py
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create Gitea PR from AI suggestions using existing gitea_integration module
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from gitea_integration import GiteaIntegration
|
||||||
|
|
||||||
|
def get_latest_pr_file():
|
||||||
|
"""Find the most recent PR file from AI suggestions"""
|
||||||
|
pr_dir = Path('/shared/ai-gitops/pending_prs')
|
||||||
|
pr_files = sorted(pr_dir.glob('*.json'), key=lambda x: x.stat().st_mtime, reverse=True)
|
||||||
|
|
||||||
|
if pr_files:
|
||||||
|
return pr_files[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Create PR from latest AI suggestions"""
|
||||||
|
print("="*60)
|
||||||
|
print(" CREATE GITEA PR FROM AI SUGGESTIONS")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
with open('/home/netops/orchestrator/config.yaml', 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Initialize Gitea integration
|
||||||
|
gitea = GiteaIntegration(config['gitea'])
|
||||||
|
|
||||||
|
# Get latest PR file
|
||||||
|
pr_file = get_latest_pr_file()
|
||||||
|
if not pr_file:
|
||||||
|
print("❌ No pending PR files found")
|
||||||
|
print(" Run: python3 run_pipeline.py --skip-netflow")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📄 Found PR file: {pr_file.name}")
|
||||||
|
|
||||||
|
# Load PR data
|
||||||
|
with open(pr_file, 'r') as f:
|
||||||
|
pr_data = json.load(f)
|
||||||
|
|
||||||
|
# Extract details
|
||||||
|
suggestions = pr_data.get('suggestions', '')
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
# Show preview
|
||||||
|
print("\n📋 PR Preview:")
|
||||||
|
print(f" Title: {pr_data.get('title', 'AI Network Optimization')}")
|
||||||
|
print(f" Model: {pr_data.get('model', 'llama2:13b')}")
|
||||||
|
print(f" Feedback aware: {pr_data.get('feedback_aware', False)}")
|
||||||
|
print(f" Config lines: {len(suggestions.split(chr(10)))}")
|
||||||
|
|
||||||
|
# Show first few lines of suggestions
|
||||||
|
print("\n📝 First few suggestions:")
|
||||||
|
for line in suggestions.split('\n')[:5]:
|
||||||
|
if line.strip():
|
||||||
|
print(f" {line}")
|
||||||
|
print(" ...")
|
||||||
|
|
||||||
|
# Confirm creation
|
||||||
|
print(f"\n❓ Create PR from these AI suggestions? (y/n): ", end="")
|
||||||
|
if input().lower() != 'y':
|
||||||
|
print("❌ Cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create PR title and description
|
||||||
|
pr_title = f"AI Network Optimization - {timestamp}"
|
||||||
|
pr_description = f"""## 🤖 AI-Generated Network Configuration
|
||||||
|
|
||||||
|
**Generated:** {timestamp}
|
||||||
|
**Model:** {pr_data.get('model', 'llama2:13b')}
|
||||||
|
**Feedback Learning:** {'✅ Applied' if pr_data.get('feedback_aware') else '❌ Not applied'}
|
||||||
|
|
||||||
|
### 📊 Security Compliance Check:
|
||||||
|
- ✅ No source-address any
|
||||||
|
- ✅ No destination-address any
|
||||||
|
- ✅ No application any
|
||||||
|
- ✅ Logging enabled
|
||||||
|
- ✅ Address-sets defined
|
||||||
|
|
||||||
|
### 📋 Configuration Summary:
|
||||||
|
This AI-generated configuration includes:
|
||||||
|
- Address-set definitions for network segmentation
|
||||||
|
- Security policies with specific source/destination
|
||||||
|
- Logging enabled for audit compliance
|
||||||
|
- No any/any/any rules (security best practice)
|
||||||
|
|
||||||
|
### 🔍 Changes Overview:
|
||||||
|
Total configuration lines: {len(suggestions.split(chr(10)))}
|
||||||
|
|
||||||
|
### 📝 Full Configuration:
|
||||||
|
```junos
|
||||||
|
{suggestions}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Review Checklist:
|
||||||
|
- [ ] Verify address-sets match network architecture
|
||||||
|
- [ ] Confirm zone assignments are correct
|
||||||
|
- [ ] Check application definitions
|
||||||
|
- [ ] Validate logging configuration
|
||||||
|
- [ ] Test in lab environment first
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by AI Network Automation System*
|
||||||
|
*Feedback learning from {pr_data.get('feedback_count', 5)} previous reviews*
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the PR
|
||||||
|
print("\n📤 Creating PR in Gitea...")
|
||||||
|
try:
|
||||||
|
pr_info = gitea.create_pr_with_config(
|
||||||
|
srx_config=suggestions,
|
||||||
|
title=pr_title,
|
||||||
|
description=pr_description
|
||||||
|
)
|
||||||
|
|
||||||
|
if pr_info:
|
||||||
|
print(f"\n✅ SUCCESS! Created PR #{pr_info['number']}")
|
||||||
|
print(f" Title: {pr_info.get('title')}")
|
||||||
|
print(f" URL: {pr_info.get('url', config['gitea']['url'] + '/' + config['gitea']['repo'] + '/pulls/' + str(pr_info['number']))}")
|
||||||
|
print(f"\n📋 Next steps:")
|
||||||
|
print(f" 1. Review PR at: {pr_info.get('url', 'Gitea URL')}")
|
||||||
|
print(f" 2. Test configuration in lab")
|
||||||
|
print(f" 3. Approve or provide feedback")
|
||||||
|
print(f" 4. If approved, run: python3 deploy_approved.py")
|
||||||
|
|
||||||
|
# Save PR tracking info
|
||||||
|
tracking_file = Path('/shared/ai-gitops/pr_tracking') / f"pr_{pr_info['number']}_created.json"
|
||||||
|
tracking_file.parent.mkdir(exist_ok=True)
|
||||||
|
with open(tracking_file, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'pr_number': pr_info['number'],
|
||||||
|
'created_at': datetime.now().isoformat(),
|
||||||
|
'pr_file': str(pr_file),
|
||||||
|
'title': pr_title,
|
||||||
|
'model': pr_data.get('model'),
|
||||||
|
'feedback_aware': pr_data.get('feedback_aware')
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create PR")
|
||||||
|
print(" Check logs for details")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating PR: {e}")
|
||||||
|
|
||||||
|
# Try to get more details
|
||||||
|
import traceback
|
||||||
|
print("\n🔍 Debug information:")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
66
scripts/orchestrator/pipeline/prepare_pr.py
Executable file
66
scripts/orchestrator/pipeline/prepare_pr.py
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Prepare PR from AI response
|
||||||
|
Converts response format to PR format
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def convert_response_to_pr():
|
||||||
|
"""Convert AI response to PR format"""
|
||||||
|
|
||||||
|
# Find latest response
|
||||||
|
response_dir = Path('/shared/ai-gitops/responses')
|
||||||
|
response_files = list(response_dir.glob('*_response.json'))
|
||||||
|
|
||||||
|
if not response_files:
|
||||||
|
print("No response files found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
latest = max(response_files, key=lambda p: p.stat().st_mtime)
|
||||||
|
print(f"Converting response: {latest.name}")
|
||||||
|
|
||||||
|
with open(latest, 'r') as f:
|
||||||
|
response = json.load(f)
|
||||||
|
|
||||||
|
# Extract suggestions and build config
|
||||||
|
suggestions = response.get('suggestions', [])
|
||||||
|
config_lines = []
|
||||||
|
|
||||||
|
for suggestion in suggestions:
|
||||||
|
if 'config' in suggestion:
|
||||||
|
config_lines.append(suggestion['config'])
|
||||||
|
|
||||||
|
if not config_lines:
|
||||||
|
print("No configuration in response")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create pending PR directory and file
|
||||||
|
pr_dir = Path('/shared/ai-gitops/pending_prs')
|
||||||
|
pr_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
pr_file = pr_dir / f"pr_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
|
||||||
|
pr_data = {
|
||||||
|
"title": f"AI Network Optimization - {response.get('focus_area', 'general').title()}",
|
||||||
|
"suggestions": '\n'.join(config_lines),
|
||||||
|
"model": "llama2:13b",
|
||||||
|
"feedback_aware": response.get('feedback_aware', True),
|
||||||
|
"feedback_count": 6,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"focus_area": response.get('focus_area', 'security')
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(pr_file, 'w') as f:
|
||||||
|
json.dump(pr_data, f, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Created PR file: {pr_file.name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if convert_response_to_pr():
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
330
scripts/orchestrator/pipeline/run_pipeline.py
Executable file
330
scripts/orchestrator/pipeline/run_pipeline.py
Executable file
@@ -0,0 +1,330 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Enhanced pipeline runner with context support for split architecture
|
||||||
|
Works with AI processor running on separate VM (192.168.100.86)
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
def load_feedback_history():
|
||||||
|
"""Load PR feedback history to understand what's already configured"""
|
||||||
|
feedback_path = Path('/shared/ai-gitops/feedback/pr_feedback_history.json')
|
||||||
|
if feedback_path.exists():
|
||||||
|
with open(feedback_path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_existing_config():
|
||||||
|
"""Load current SRX config to identify already-configured features"""
|
||||||
|
config_path = Path('/shared/ai-gitops/configs/current_srx_config.conf')
|
||||||
|
if config_path.exists():
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def build_ai_context(args):
|
||||||
|
"""Build comprehensive context for AI based on arguments and history"""
|
||||||
|
context = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"focus_area": args.context,
|
||||||
|
"skip_basic": True, # Always skip basic connectivity suggestions
|
||||||
|
"existing_features": [],
|
||||||
|
"priority_features": [],
|
||||||
|
"constraints": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load existing configuration to prevent redundant suggestions
|
||||||
|
current_config = load_existing_config()
|
||||||
|
|
||||||
|
# Identify already-configured features
|
||||||
|
if "security-zone" in current_config:
|
||||||
|
context["existing_features"].append("zones_configured")
|
||||||
|
if "port-forwarding" in current_config:
|
||||||
|
context["existing_features"].append("gaming_optimizations")
|
||||||
|
if "wireguard" in current_config.lower():
|
||||||
|
context["existing_features"].append("vpn_configured")
|
||||||
|
|
||||||
|
# Set priorities based on context argument
|
||||||
|
if args.context == "performance":
|
||||||
|
context["priority_features"] = [
|
||||||
|
"qos_policies",
|
||||||
|
"traffic_shaping",
|
||||||
|
"bandwidth_management",
|
||||||
|
"flow_optimization"
|
||||||
|
]
|
||||||
|
context["constraints"].append("Focus on QoS and traffic optimization")
|
||||||
|
|
||||||
|
elif args.context == "security":
|
||||||
|
context["priority_features"] = [
|
||||||
|
"rate_limiting",
|
||||||
|
"ddos_protection",
|
||||||
|
"ids_ips_rules",
|
||||||
|
"geo_blocking",
|
||||||
|
"threat_feeds"
|
||||||
|
]
|
||||||
|
context["constraints"].append("Focus on advanced security features")
|
||||||
|
|
||||||
|
elif args.context == "monitoring":
|
||||||
|
context["priority_features"] = [
|
||||||
|
"syslog_enhancements",
|
||||||
|
"snmp_traps",
|
||||||
|
"flow_analytics",
|
||||||
|
"performance_metrics"
|
||||||
|
]
|
||||||
|
context["constraints"].append("Focus on visibility and monitoring")
|
||||||
|
|
||||||
|
elif args.context == "automation":
|
||||||
|
context["priority_features"] = [
|
||||||
|
"event_scripts",
|
||||||
|
"automated_responses",
|
||||||
|
"dynamic_policies",
|
||||||
|
"api_integrations"
|
||||||
|
]
|
||||||
|
context["constraints"].append("Focus on automation capabilities")
|
||||||
|
|
||||||
|
# Add learned constraints from feedback history
|
||||||
|
feedback = load_feedback_history()
|
||||||
|
if feedback:
|
||||||
|
# Extract patterns AI should avoid
|
||||||
|
rejected_patterns = []
|
||||||
|
for entry in feedback:
|
||||||
|
if entry.get("status") == "rejected" or entry.get("feedback_type") == "rejected":
|
||||||
|
rejected_patterns.append(entry.get("reason", ""))
|
||||||
|
|
||||||
|
if rejected_patterns:
|
||||||
|
context["constraints"].append("Avoid patterns that were previously rejected")
|
||||||
|
context["rejected_patterns"] = rejected_patterns[-5:] # Last 5 rejections
|
||||||
|
|
||||||
|
# Add instruction to avoid redundant suggestions
|
||||||
|
context["instructions"] = [
|
||||||
|
"DO NOT suggest basic connectivity policies - all zones are properly configured",
|
||||||
|
"DO NOT suggest any/any/any rules - this has been rejected multiple times",
|
||||||
|
"FOCUS on advanced features that enhance the existing configuration",
|
||||||
|
"CHECK if feature already exists before suggesting",
|
||||||
|
f"PRIORITY: {args.context} optimizations and enhancements"
|
||||||
|
]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def run_collection():
|
||||||
|
"""Run the config collection script"""
|
||||||
|
print("📊 Collecting current SRX configuration...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", "/home/netops/orchestrator/collect_srx_config.py"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Collection failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
print("✅ Configuration collected successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_ai_request(context):
|
||||||
|
"""Create an AI analysis request in the shared directory"""
|
||||||
|
print(f"🤖 Creating AI analysis request with context: {context['focus_area']}...")
|
||||||
|
|
||||||
|
# Generate unique request ID
|
||||||
|
request_id = f"pipeline_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# Create request data
|
||||||
|
request_data = {
|
||||||
|
"request_id": request_id,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"type": "analyze_config",
|
||||||
|
"context": context,
|
||||||
|
"data": {
|
||||||
|
"config_file": "/shared/ai-gitops/configs/srx_config_latest.txt",
|
||||||
|
"analysis_file": "/shared/ai-gitops/configs/srx_config_analysis_latest.json",
|
||||||
|
"top_talkers": {"buckets": []}, # Empty for context-based analysis
|
||||||
|
"vlans": {"buckets": []},
|
||||||
|
"protocols": {"buckets": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save context to shared location
|
||||||
|
context_path = Path('/shared/ai-gitops/context/current_context.json')
|
||||||
|
context_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(context_path, 'w') as f:
|
||||||
|
json.dump(context, f, indent=2)
|
||||||
|
|
||||||
|
# Save request to trigger AI processor
|
||||||
|
request_path = Path('/shared/ai-gitops/requests') / f"{request_id}.json"
|
||||||
|
request_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(request_path, 'w') as f:
|
||||||
|
json.dump(request_data, f, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Request created: {request_id}")
|
||||||
|
return request_id
|
||||||
|
|
||||||
|
def wait_for_ai_response(request_id, timeout=60):
|
||||||
|
"""Wait for AI processor to complete analysis"""
|
||||||
|
print(f"⏳ Waiting for AI processor response (timeout: {timeout}s)...")
|
||||||
|
|
||||||
|
response_path = Path('/shared/ai-gitops/responses') / f"{request_id}_response.json"
|
||||||
|
|
||||||
|
for i in range(timeout):
|
||||||
|
if response_path.exists():
|
||||||
|
print("✅ AI analysis completed")
|
||||||
|
|
||||||
|
# Read and display key info
|
||||||
|
with open(response_path, 'r') as f:
|
||||||
|
response = json.load(f)
|
||||||
|
|
||||||
|
if 'focus_area' in response:
|
||||||
|
print(f" Focus area: {response['focus_area']}")
|
||||||
|
if 'feedback_aware' in response:
|
||||||
|
print(f" Feedback aware: {response['feedback_aware']}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Show progress every 5 seconds
|
||||||
|
if i % 5 == 0 and i > 0:
|
||||||
|
print(f" Still waiting... ({i}/{timeout}s)")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print(f"❌ Timeout waiting for AI response after {timeout} seconds")
|
||||||
|
print(" Check AI processor logs: ssh netops@192.168.100.86 'sudo tail /var/log/ai-processor/ai-processor.log'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_pr():
|
||||||
|
"""Create pull request in Gitea"""
|
||||||
|
print("📝 Creating pull request...")
|
||||||
|
|
||||||
|
# Check if create_ai_pr.py exists
|
||||||
|
create_pr_script = Path('/home/netops/orchestrator/create_ai_pr.py')
|
||||||
|
if not create_pr_script.exists():
|
||||||
|
print("❌ create_ai_pr.py not found - using placeholder")
|
||||||
|
print(" To create PRs, ensure create_ai_pr.py is available")
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", str(create_pr_script)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ PR creation failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
print("✅ Pull request created")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_ai_processor_status():
|
||||||
|
"""Check if AI processor service is running on remote VM"""
|
||||||
|
print("🔍 Checking AI processor status...")
|
||||||
|
|
||||||
|
# Try without sudo first (systemctl can check status without sudo)
|
||||||
|
result = subprocess.run(
|
||||||
|
["ssh", "netops@192.168.100.86", "systemctl is-active ai-processor"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.stdout.strip() == "active":
|
||||||
|
print("✅ AI processor service is running")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Try checking if the process is running another way
|
||||||
|
result = subprocess.run(
|
||||||
|
["ssh", "netops@192.168.100.86", "ps aux | grep -v grep | grep ai_processor"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if "ai_processor.py" in result.stdout:
|
||||||
|
print("✅ AI processor is running (detected via process)")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("⚠️ Cannot verify AI processor status (but it may still be running)")
|
||||||
|
print(" Continuing anyway...")
|
||||||
|
return True # Continue anyway since we know it's running
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Run AI-driven network optimization pipeline with context'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--context',
|
||||||
|
choices=['performance', 'security', 'monitoring', 'automation'],
|
||||||
|
default='security',
|
||||||
|
help='Focus area for AI analysis (default: security)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--skip-collection',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip config collection (use existing)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='Run analysis but do not create PR'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable verbose output'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--timeout',
|
||||||
|
type=int,
|
||||||
|
default=60,
|
||||||
|
help='Timeout waiting for AI response (default: 60s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"🚀 Starting pipeline with context: {args.context}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Step 0: Check AI processor is running
|
||||||
|
if not check_ai_processor_status():
|
||||||
|
print("\n⚠️ Please start the AI processor service first")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 1: Collect current config (unless skipped)
|
||||||
|
if not args.skip_collection:
|
||||||
|
if not run_collection():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 2: Build context for AI
|
||||||
|
context = build_ai_context(args)
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
print("\n📋 AI Context:")
|
||||||
|
print(json.dumps(context, indent=2))
|
||||||
|
|
||||||
|
# Step 3: Create AI request (this triggers the remote AI processor)
|
||||||
|
request_id = create_ai_request(context)
|
||||||
|
|
||||||
|
# Step 4: Wait for AI processor to complete
|
||||||
|
if not wait_for_ai_response(request_id, args.timeout):
|
||||||
|
print("\n⚠️ AI processor may be busy or not running properly")
|
||||||
|
print(" Check status: ssh netops@192.168.100.86 'sudo systemctl status ai-processor'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 5: Create PR (unless dry-run)
|
||||||
|
if not args.dry_run:
|
||||||
|
if not create_pr():
|
||||||
|
print("⚠️ PR creation failed but analysis is complete")
|
||||||
|
print(f" View results: cat /shared/ai-gitops/responses/{request_id}_response.json")
|
||||||
|
else:
|
||||||
|
print("⚡ Dry run - skipping PR creation")
|
||||||
|
print(f" View analysis: cat /shared/ai-gitops/responses/{request_id}_response.json | jq .suggestions")
|
||||||
|
|
||||||
|
print("\n✨ Pipeline completed successfully!")
|
||||||
|
print(f"Focus area: {args.context}")
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
print("Next steps: Review the PR in Gitea")
|
||||||
|
else:
|
||||||
|
print(f"Next steps: Review the suggestions and run without --dry-run to create PR")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
317
scripts/orchestrator/srx/collect_srx_config.py
Executable file
317
scripts/orchestrator/srx/collect_srx_config.py
Executable file
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
SRX Configuration Collector
|
||||||
|
Pulls current configuration from SRX and stores it for AI analysis
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import paramiko
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SRXConfigCollector:
|
||||||
|
def __init__(self, config_path='/home/netops/orchestrator/config.yaml'):
|
||||||
|
"""Initialize with orchestrator config"""
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
self.srx_config = self.config['srx']
|
||||||
|
self.config_dir = Path('/shared/ai-gitops/configs')
|
||||||
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def connect_to_srx(self):
|
||||||
|
"""Establish SSH connection to SRX"""
|
||||||
|
try:
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
# Connect using SSH key
|
||||||
|
client.connect(
|
||||||
|
hostname=self.srx_config['host'],
|
||||||
|
username=self.srx_config['username'],
|
||||||
|
key_filename=self.srx_config['ssh_key'],
|
||||||
|
port=22
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Connected to SRX at {self.srx_config['host']}")
|
||||||
|
return client
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_full_config(self, client):
|
||||||
|
"""Get complete SRX configuration"""
|
||||||
|
logger.info("Fetching full SRX configuration...")
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command('show configuration | no-more')
|
||||||
|
config_output = stdout.read().decode('utf-8')
|
||||||
|
|
||||||
|
if config_output:
|
||||||
|
logger.info(f"Retrieved {len(config_output)} bytes of configuration")
|
||||||
|
return config_output
|
||||||
|
else:
|
||||||
|
logger.error("Failed to retrieve configuration")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_security_config(self, client):
|
||||||
|
"""Get security-specific configuration"""
|
||||||
|
logger.info("Fetching security policies...")
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
'show configuration security policies',
|
||||||
|
'show configuration security zones',
|
||||||
|
'show configuration security address-book',
|
||||||
|
'show configuration applications',
|
||||||
|
'show configuration security nat',
|
||||||
|
'show configuration interfaces'
|
||||||
|
]
|
||||||
|
|
||||||
|
security_config = {}
|
||||||
|
|
||||||
|
for cmd in commands:
|
||||||
|
stdin, stdout, stderr = client.exec_command(f'{cmd} | no-more')
|
||||||
|
output = stdout.read().decode('utf-8')
|
||||||
|
section = cmd.split()[-1] # Get last word as section name
|
||||||
|
security_config[section] = output
|
||||||
|
logger.info(f"Retrieved {section} configuration")
|
||||||
|
|
||||||
|
return security_config
|
||||||
|
|
||||||
|
def analyze_config(self, full_config, security_config):
|
||||||
|
"""Analyze configuration and extract key information - FIXED VERSION"""
|
||||||
|
analysis = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'zones': [],
|
||||||
|
'networks': {},
|
||||||
|
'policies': [],
|
||||||
|
'policy_count': 0,
|
||||||
|
'applications': [],
|
||||||
|
'interfaces': {},
|
||||||
|
'nat_rules': [],
|
||||||
|
'address_book': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract zones - FIXED parsing for your format
|
||||||
|
if 'zones' in security_config:
|
||||||
|
zones_content = security_config['zones']
|
||||||
|
if zones_content:
|
||||||
|
lines = zones_content.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
# Your format: "security-zone WAN {" or "security-zone HOME {"
|
||||||
|
if 'security-zone' in line and '{' in line:
|
||||||
|
# Extract zone name between 'security-zone' and '{'
|
||||||
|
parts = line.strip().split()
|
||||||
|
if len(parts) >= 2 and parts[0] == 'security-zone':
|
||||||
|
zone_name = parts[1]
|
||||||
|
if zone_name != '{': # Make sure it's not just the bracket
|
||||||
|
analysis['zones'].append(zone_name)
|
||||||
|
analysis['networks'][zone_name] = []
|
||||||
|
|
||||||
|
# Extract address-book entries from zones section
|
||||||
|
if 'zones' in security_config:
|
||||||
|
lines = security_config['zones'].split('\n')
|
||||||
|
current_zone = None
|
||||||
|
in_address_book = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Track current zone
|
||||||
|
if 'security-zone' in line and '{' in line:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
current_zone = parts[1]
|
||||||
|
in_address_book = False
|
||||||
|
|
||||||
|
# Check if we're in address-book section
|
||||||
|
elif 'address-book' in line and '{' in line:
|
||||||
|
in_address_book = True
|
||||||
|
|
||||||
|
# Parse addresses within address-book
|
||||||
|
elif in_address_book and 'address ' in line and current_zone:
|
||||||
|
# Format: "address GAMING-NETWORK 192.168.10.0/24;"
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3 and parts[0] == 'address':
|
||||||
|
addr_name = parts[1]
|
||||||
|
addr_value = parts[2].rstrip(';')
|
||||||
|
if '/' in addr_value or '.' in addr_value:
|
||||||
|
analysis['address_book'][addr_name] = addr_value
|
||||||
|
if current_zone in analysis['networks']:
|
||||||
|
analysis['networks'][current_zone].append(addr_value)
|
||||||
|
|
||||||
|
# Extract policies - FIXED for your format
|
||||||
|
if 'policies' in security_config:
|
||||||
|
policies_content = security_config['policies']
|
||||||
|
if policies_content:
|
||||||
|
lines = policies_content.split('\n')
|
||||||
|
from_zone = None
|
||||||
|
to_zone = None
|
||||||
|
current_policy = None
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Format: "from-zone HOME to-zone WAN {"
|
||||||
|
if 'from-zone' in line and 'to-zone' in line:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
from_idx = parts.index('from-zone') if 'from-zone' in parts else -1
|
||||||
|
to_idx = parts.index('to-zone') if 'to-zone' in parts else -1
|
||||||
|
if from_idx >= 0 and to_idx >= 0:
|
||||||
|
from_zone = parts[from_idx + 1] if from_idx + 1 < len(parts) else None
|
||||||
|
to_zone = parts[to_idx + 1] if to_idx + 1 < len(parts) else None
|
||||||
|
to_zone = to_zone.rstrip('{') if to_zone else None
|
||||||
|
|
||||||
|
# Format: "policy GAMING-VLAN-PRIORITY {"
|
||||||
|
elif 'policy ' in line and '{' in line and from_zone and to_zone:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2 and parts[0] == 'policy':
|
||||||
|
policy_name = parts[1].rstrip('{')
|
||||||
|
analysis['policies'].append({
|
||||||
|
'name': policy_name,
|
||||||
|
'from_zone': from_zone,
|
||||||
|
'to_zone': to_zone
|
||||||
|
})
|
||||||
|
analysis['policy_count'] += 1
|
||||||
|
|
||||||
|
# Extract applications
|
||||||
|
if 'applications' in security_config:
|
||||||
|
apps_content = security_config['applications']
|
||||||
|
if apps_content:
|
||||||
|
lines = apps_content.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
# Format: "application PS5-HTTP {"
|
||||||
|
if 'application ' in line and '{' in line:
|
||||||
|
parts = line.strip().split()
|
||||||
|
if len(parts) >= 2 and parts[0] == 'application':
|
||||||
|
app_name = parts[1].rstrip('{')
|
||||||
|
if app_name and app_name != 'application':
|
||||||
|
analysis['applications'].append(app_name)
|
||||||
|
|
||||||
|
# Extract interfaces with IPs
|
||||||
|
if 'interfaces' in security_config:
|
||||||
|
interfaces_content = security_config['interfaces']
|
||||||
|
if interfaces_content:
|
||||||
|
lines = interfaces_content.split('\n')
|
||||||
|
current_interface = None
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Interface line (e.g., "ge-0/0/0 {" or "reth0 {")
|
||||||
|
if (line.startswith('ge-') or line.startswith('reth')) and '{' in line:
|
||||||
|
current_interface = line.split()[0]
|
||||||
|
analysis['interfaces'][current_interface] = {'addresses': []}
|
||||||
|
|
||||||
|
# IP address line (e.g., "address 192.168.1.1/24;")
|
||||||
|
elif current_interface and 'address ' in line and '/' in line:
|
||||||
|
parts = line.split()
|
||||||
|
for part in parts:
|
||||||
|
if '/' in part:
|
||||||
|
addr = part.rstrip(';')
|
||||||
|
analysis['interfaces'][current_interface]['addresses'].append(addr)
|
||||||
|
|
||||||
|
# Extract NAT rules
|
||||||
|
if 'nat' in security_config:
|
||||||
|
nat_content = security_config['nat']
|
||||||
|
if nat_content:
|
||||||
|
source_nat_count = nat_content.count('source pool')
|
||||||
|
dest_nat_count = nat_content.count('destination pool')
|
||||||
|
analysis['nat_rules'] = {
|
||||||
|
'source_nat': source_nat_count,
|
||||||
|
'destination_nat': dest_nat_count,
|
||||||
|
'total': source_nat_count + dest_nat_count
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
def save_config(self, full_config, security_config, analysis):
|
||||||
|
"""Save configuration and analysis"""
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
|
||||||
|
# Save full config
|
||||||
|
full_config_path = self.config_dir / f'srx_config_{timestamp}.txt'
|
||||||
|
with open(full_config_path, 'w') as f:
|
||||||
|
f.write(full_config)
|
||||||
|
logger.info(f"Saved full config to {full_config_path}")
|
||||||
|
|
||||||
|
# Save latest symlink
|
||||||
|
latest_path = self.config_dir / 'srx_config_latest.txt'
|
||||||
|
if latest_path.exists():
|
||||||
|
latest_path.unlink()
|
||||||
|
latest_path.symlink_to(full_config_path.name)
|
||||||
|
|
||||||
|
# Save security config sections
|
||||||
|
security_config_path = self.config_dir / f'srx_security_config_{timestamp}.json'
|
||||||
|
with open(security_config_path, 'w') as f:
|
||||||
|
json.dump(security_config, f, indent=2)
|
||||||
|
|
||||||
|
# Save analysis
|
||||||
|
analysis_path = self.config_dir / f'srx_config_analysis_{timestamp}.json'
|
||||||
|
with open(analysis_path, 'w') as f:
|
||||||
|
json.dump(analysis, f, indent=2)
|
||||||
|
logger.info(f"Saved config analysis to {analysis_path}")
|
||||||
|
|
||||||
|
# Save latest analysis symlink
|
||||||
|
latest_analysis = self.config_dir / 'srx_config_analysis_latest.json'
|
||||||
|
if latest_analysis.exists():
|
||||||
|
latest_analysis.unlink()
|
||||||
|
latest_analysis.symlink_to(analysis_path.name)
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
def collect(self):
|
||||||
|
"""Main collection process"""
|
||||||
|
logger.info("Starting SRX configuration collection...")
|
||||||
|
|
||||||
|
# Connect to SRX
|
||||||
|
client = self.connect_to_srx()
|
||||||
|
if not client:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get configurations
|
||||||
|
full_config = self.get_full_config(client)
|
||||||
|
security_config = self.get_security_config(client)
|
||||||
|
|
||||||
|
if full_config:
|
||||||
|
# Analyze configuration
|
||||||
|
analysis = self.analyze_config(full_config, security_config)
|
||||||
|
|
||||||
|
# Save everything
|
||||||
|
self.save_config(full_config, security_config, analysis)
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n📊 Configuration Summary:")
|
||||||
|
print(f"Zones: {', '.join(analysis['zones'])}")
|
||||||
|
print(f"Networks: {len([n for nets in analysis['networks'].values() for n in nets])} subnets across {len(analysis['zones'])} zones")
|
||||||
|
print(f"Policies: {analysis.get('policy_count', 0)} security policies")
|
||||||
|
print(f"Address Book: {len(analysis['address_book'])} entries")
|
||||||
|
print(f"Interfaces: {len(analysis['interfaces'])} configured")
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
logger.info("Disconnected from SRX")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
collector = SRXConfigCollector()
|
||||||
|
analysis = collector.collect()
|
||||||
|
|
||||||
|
if analysis:
|
||||||
|
print("\n✅ Configuration collected successfully!")
|
||||||
|
print(f"Files saved in: /shared/ai-gitops/configs/")
|
||||||
|
else:
|
||||||
|
print("\n❌ Failed to collect configuration")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
28
scripts/orchestrator/srx/deploy_approved.py
Executable file
28
scripts/orchestrator/srx/deploy_approved.py
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple deployment script placeholder
|
||||||
|
Full version will deploy approved configs
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('/var/log/orchestrator/deployment.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("Deployment check started")
|
||||||
|
logger.info("Looking for approved configurations...")
|
||||||
|
# TODO: Implement actual deployment logic
|
||||||
|
logger.info("No approved configurations found")
|
||||||
|
logger.info("Deployment check complete")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
309
scripts/orchestrator/srx/srx_manager.py
Executable file
309
scripts/orchestrator/srx/srx_manager.py
Executable file
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
SRX Configuration Manager
|
||||||
|
Handles all interactions with the Juniper SRX device
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SRXManager:
|
||||||
|
"""Manages SRX configuration retrieval and deployment"""
|
||||||
|
|
||||||
|
def __init__(self, host: str, user: str, ssh_key: str):
|
||||||
|
"""
|
||||||
|
Initialize SRX Manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: SRX IP address
|
||||||
|
user: SSH username
|
||||||
|
ssh_key: Path to SSH private key
|
||||||
|
"""
|
||||||
|
self.host = host
|
||||||
|
self.user = user
|
||||||
|
self.ssh_key = ssh_key
|
||||||
|
|
||||||
|
def _execute_ssh_command(self, command: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Execute command on SRX via SSH
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(success, output) tuple
|
||||||
|
"""
|
||||||
|
ssh_cmd = [
|
||||||
|
'ssh',
|
||||||
|
'-i', self.ssh_key,
|
||||||
|
'-o', 'StrictHostKeyChecking=no',
|
||||||
|
'-o', 'ConnectTimeout=10',
|
||||||
|
f'{self.user}@{self.host}',
|
||||||
|
command
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
ssh_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Successfully executed: {command[:50]}...")
|
||||||
|
return True, result.stdout
|
||||||
|
else:
|
||||||
|
logger.error(f"Command failed: {result.stderr}")
|
||||||
|
return False, result.stderr
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("SSH command timed out")
|
||||||
|
return False, "Command timed out"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SSH execution error: {e}")
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def get_current_config(self, format: str = "set") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Retrieve current SRX configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
format: Configuration format ('set', 'json', 'xml')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration string or None if failed
|
||||||
|
"""
|
||||||
|
format_map = {
|
||||||
|
"set": "display set",
|
||||||
|
"json": "display json",
|
||||||
|
"xml": "display xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
display_format = format_map.get(format, "display set")
|
||||||
|
command = f"show configuration | {display_format} | no-more"
|
||||||
|
|
||||||
|
logger.info(f"Pulling SRX configuration in {format} format")
|
||||||
|
success, output = self._execute_ssh_command(command)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"Retrieved {len(output)} characters of configuration")
|
||||||
|
return output
|
||||||
|
else:
|
||||||
|
logger.error("Failed to retrieve configuration")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_config_section(self, section: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get specific configuration section
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section: Config section (e.g., 'security policies', 'interfaces')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration section or None
|
||||||
|
"""
|
||||||
|
command = f"show configuration {section} | display set | no-more"
|
||||||
|
success, output = self._execute_ssh_command(command)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return output
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_security_policies(self, config: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Parse security policies from configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of policies organized by zones
|
||||||
|
"""
|
||||||
|
policies = {
|
||||||
|
"zone_pairs": {},
|
||||||
|
"total_policies": 0,
|
||||||
|
"applications": set(),
|
||||||
|
"addresses": set()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Regex patterns for parsing
|
||||||
|
policy_pattern = r'set security policies from-zone (\S+) to-zone (\S+) policy (\S+)'
|
||||||
|
app_pattern = r'set security policies .* application (\S+)'
|
||||||
|
addr_pattern = r'set security policies .* (source|destination)-address (\S+)'
|
||||||
|
|
||||||
|
for line in config.split('\n'):
|
||||||
|
# Parse policy definitions
|
||||||
|
policy_match = re.match(policy_pattern, line)
|
||||||
|
if policy_match:
|
||||||
|
from_zone, to_zone, policy_name = policy_match.groups()
|
||||||
|
zone_pair = f"{from_zone}->{to_zone}"
|
||||||
|
|
||||||
|
if zone_pair not in policies["zone_pairs"]:
|
||||||
|
policies["zone_pairs"][zone_pair] = []
|
||||||
|
|
||||||
|
if policy_name not in policies["zone_pairs"][zone_pair]:
|
||||||
|
policies["zone_pairs"][zone_pair].append(policy_name)
|
||||||
|
policies["total_policies"] += 1
|
||||||
|
|
||||||
|
# Parse applications
|
||||||
|
app_match = re.search(app_pattern, line)
|
||||||
|
if app_match:
|
||||||
|
policies["applications"].add(app_match.group(1))
|
||||||
|
|
||||||
|
# Parse addresses
|
||||||
|
addr_match = re.search(addr_pattern, line)
|
||||||
|
if addr_match:
|
||||||
|
policies["addresses"].add(addr_match.group(2))
|
||||||
|
|
||||||
|
# Convert sets to lists for JSON serialization
|
||||||
|
policies["applications"] = list(policies["applications"])
|
||||||
|
policies["addresses"] = list(policies["addresses"])
|
||||||
|
|
||||||
|
return policies
|
||||||
|
|
||||||
|
def validate_config_syntax(self, config_lines: List[str]) -> tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate SRX configuration syntax
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_lines: List of configuration commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(valid, errors) tuple
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
valid_commands = [
|
||||||
|
'set security policies',
|
||||||
|
'set security zones',
|
||||||
|
'set security address-book',
|
||||||
|
'set applications application',
|
||||||
|
'set firewall policer',
|
||||||
|
'set firewall filter',
|
||||||
|
'set class-of-service',
|
||||||
|
'set interfaces',
|
||||||
|
'set routing-options'
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, line in enumerate(config_lines, 1):
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Skip comments and empty lines
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if line starts with valid command
|
||||||
|
if not any(line.startswith(cmd) for cmd in valid_commands):
|
||||||
|
errors.append(f"Line {i}: Invalid command prefix: {line[:50]}")
|
||||||
|
|
||||||
|
# Check for required keywords in policies
|
||||||
|
if 'security policies' in line and 'policy' in line:
|
||||||
|
if not any(keyword in line for keyword in ['match', 'then', 'from-zone', 'to-zone']):
|
||||||
|
errors.append(f"Line {i}: Policy missing required keywords: {line[:50]}")
|
||||||
|
|
||||||
|
return len(errors) == 0, errors
|
||||||
|
|
||||||
|
def test_connectivity(self) -> bool:
|
||||||
|
"""
|
||||||
|
Test SSH connectivity to SRX
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if connected successfully
|
||||||
|
"""
|
||||||
|
logger.info(f"Testing connectivity to {self.host}")
|
||||||
|
success, output = self._execute_ssh_command("show version | match Junos:")
|
||||||
|
|
||||||
|
if success and "Junos:" in output:
|
||||||
|
version = output.strip()
|
||||||
|
logger.info(f"Connected successfully: {version}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Connectivity test failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_traffic_statistics(self) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Get interface traffic statistics
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of traffic stats or None
|
||||||
|
"""
|
||||||
|
command = "show interfaces statistics | display json"
|
||||||
|
success, output = self._execute_ssh_command(command)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
try:
|
||||||
|
# Parse JSON output
|
||||||
|
stats = json.loads(output)
|
||||||
|
return stats
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error("Failed to parse traffic statistics JSON")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_config_diff(self, current_config: str, proposed_config: List[str]) -> Dict:
|
||||||
|
"""
|
||||||
|
Create a diff between current and proposed configurations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_config: Current SRX configuration
|
||||||
|
proposed_config: List of proposed configuration lines
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with additions and analysis
|
||||||
|
"""
|
||||||
|
current_lines = set(current_config.split('\n'))
|
||||||
|
proposed_set = set(proposed_config)
|
||||||
|
|
||||||
|
# Find truly new configurations
|
||||||
|
new_configs = []
|
||||||
|
duplicate_configs = []
|
||||||
|
|
||||||
|
for config in proposed_set:
|
||||||
|
if config.strip() and not config.startswith('#'):
|
||||||
|
if config not in current_lines:
|
||||||
|
new_configs.append(config)
|
||||||
|
else:
|
||||||
|
duplicate_configs.append(config)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"new_configurations": new_configs,
|
||||||
|
"duplicate_configurations": duplicate_configs,
|
||||||
|
"total_proposed": len(proposed_config),
|
||||||
|
"total_new": len(new_configs),
|
||||||
|
"total_duplicates": len(duplicate_configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Test function for standalone execution
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the SRX Manager
|
||||||
|
srx = SRXManager(
|
||||||
|
host="192.168.100.1",
|
||||||
|
user="netops",
|
||||||
|
ssh_key="/home/netops/.ssh/srx_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test connectivity
|
||||||
|
if srx.test_connectivity():
|
||||||
|
print("✅ Connectivity test passed")
|
||||||
|
|
||||||
|
# Get current config
|
||||||
|
config = srx.get_current_config()
|
||||||
|
if config:
|
||||||
|
print(f"✅ Retrieved {len(config)} characters of configuration")
|
||||||
|
|
||||||
|
# Parse policies
|
||||||
|
policies = srx.parse_security_policies(config)
|
||||||
|
print(f"📊 Found {policies['total_policies']} security policies")
|
||||||
|
print(f"📊 Zone pairs: {list(policies['zone_pairs'].keys())}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to retrieve configuration")
|
||||||
|
else:
|
||||||
|
print("❌ Connectivity test failed")
|
||||||
Reference in New Issue
Block a user