- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
427 lines
16 KiB
Python
Executable File
427 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Proxmox Review and Deployment Planning Script
|
|
Connects to both Proxmox instances, reviews configurations, checks status,
|
|
and generates a deployment plan with detailed task list.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import requests
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
from urllib.parse import urlparse
|
|
|
|
# Try to import proxmoxer, but fall back to direct API calls if not available
|
|
try:
|
|
from proxmoxer import ProxmoxAPI
|
|
PROXMOXER_AVAILABLE = True
|
|
except ImportError:
|
|
PROXMOXER_AVAILABLE = False
|
|
print("Warning: proxmoxer not installed. Using direct API calls.")
|
|
print("Install with: pip install proxmoxer")
|
|
|
|
# Colors for terminal output
|
|
class Colors:
|
|
RED = '\033[0;31m'
|
|
GREEN = '\033[0;32m'
|
|
YELLOW = '\033[1;33m'
|
|
BLUE = '\033[0;34m'
|
|
CYAN = '\033[0;36m'
|
|
NC = '\033[0m' # No Color
|
|
|
|
def log(message: str, color: str = Colors.BLUE):
|
|
"""Print a log message with timestamp."""
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
print(f"{color}[{timestamp}]{Colors.NC} {message}")
|
|
|
|
def log_success(message: str):
|
|
"""Print a success message."""
|
|
log(f"✅ {message}", Colors.GREEN)
|
|
|
|
def log_warning(message: str):
|
|
"""Print a warning message."""
|
|
log(f"⚠️ {message}", Colors.YELLOW)
|
|
|
|
def log_error(message: str):
|
|
"""Print an error message."""
|
|
log(f"❌ {message}", Colors.RED)
|
|
|
|
class ProxmoxClient:
|
|
"""Proxmox API client."""
|
|
|
|
def __init__(self, api_url: str, username: str = None, password: str = None,
|
|
token: str = None, verify_ssl: bool = True):
|
|
self.api_url = api_url.rstrip('/')
|
|
self.username = username
|
|
self.password = password
|
|
self.token = token
|
|
self.verify_ssl = verify_ssl
|
|
self.session = requests.Session()
|
|
self.ticket = None
|
|
self.csrf_token = None
|
|
|
|
if not verify_ssl:
|
|
import urllib3
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
def authenticate(self) -> bool:
|
|
"""Authenticate to Proxmox API."""
|
|
if self.token:
|
|
# Token authentication
|
|
self.session.headers['Authorization'] = f'PVEAuthCookie={self.token}'
|
|
return True
|
|
|
|
if not self.username or not self.password:
|
|
log_error("Username/password or token required")
|
|
return False
|
|
|
|
# Password authentication
|
|
auth_url = f"{self.api_url}/api2/json/access/ticket"
|
|
try:
|
|
response = self.session.post(
|
|
auth_url,
|
|
data={'username': self.username, 'password': self.password},
|
|
verify=self.verify_ssl,
|
|
timeout=10
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()['data']
|
|
self.ticket = data['ticket']
|
|
self.csrf_token = data.get('CSRFPreventionToken', '')
|
|
self.session.headers['Cookie'] = f'PVEAuthCookie={self.ticket}'
|
|
if self.csrf_token:
|
|
self.session.headers['CSRFPreventionToken'] = self.csrf_token
|
|
return True
|
|
except Exception as e:
|
|
log_error(f"Authentication failed: {e}")
|
|
return False
|
|
|
|
def api_call(self, endpoint: str, method: str = 'GET', data: dict = None) -> Optional[dict]:
|
|
"""Make an API call to Proxmox."""
|
|
url = f"{self.api_url}/api2/json{endpoint}"
|
|
try:
|
|
if method == 'GET':
|
|
response = self.session.get(url, verify=self.verify_ssl, timeout=10)
|
|
elif method == 'POST':
|
|
response = self.session.post(url, json=data, verify=self.verify_ssl, timeout=10)
|
|
elif method == 'PUT':
|
|
response = self.session.put(url, json=data, verify=self.verify_ssl, timeout=10)
|
|
elif method == 'DELETE':
|
|
response = self.session.delete(url, verify=self.verify_ssl, timeout=10)
|
|
else:
|
|
log_error(f"Unsupported HTTP method: {method}")
|
|
return None
|
|
|
|
response.raise_for_status()
|
|
return response.json().get('data')
|
|
except Exception as e:
|
|
log_error(f"API call failed ({endpoint}): {e}")
|
|
return None
|
|
|
|
def get_version(self) -> Optional[dict]:
|
|
"""Get Proxmox version information."""
|
|
return self.api_call('/version')
|
|
|
|
def get_cluster_status(self) -> Optional[List[dict]]:
|
|
"""Get cluster status."""
|
|
return self.api_call('/cluster/status')
|
|
|
|
def get_nodes(self) -> Optional[List[dict]]:
|
|
"""Get list of nodes."""
|
|
return self.api_call('/nodes')
|
|
|
|
def get_node_status(self, node: str) -> Optional[dict]:
|
|
"""Get status of a specific node."""
|
|
return self.api_call(f'/nodes/{node}/status')
|
|
|
|
def get_storage(self) -> Optional[List[dict]]:
|
|
"""Get storage information."""
|
|
return self.api_call('/storage')
|
|
|
|
def get_vms(self, node: str) -> Optional[List[dict]]:
|
|
"""Get VMs on a node."""
|
|
return self.api_call(f'/nodes/{node}/qemu')
|
|
|
|
def get_networks(self, node: str) -> Optional[List[dict]]:
|
|
"""Get network configuration for a node."""
|
|
return self.api_call(f'/nodes/{node}/network')
|
|
|
|
def load_environment():
|
|
"""Load environment variables."""
|
|
env_file = Path(__file__).parent.parent / '.env'
|
|
env_vars = {}
|
|
|
|
if env_file.exists():
|
|
with open(env_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
env_vars[key.strip()] = value.strip()
|
|
|
|
# Set defaults
|
|
config = {
|
|
'proxmox_1': {
|
|
'api_url': env_vars.get('PROXMOX_1_API_URL', 'https://192.168.11.10:8006'),
|
|
'user': env_vars.get('PROXMOX_1_USER', 'root'),
|
|
'password': env_vars.get('PROXMOX_1_PASS', ''),
|
|
'token': env_vars.get('PROXMOX_1_API_TOKEN', ''),
|
|
'verify_ssl': env_vars.get('PROXMOX_1_INSECURE_SKIP_TLS_VERIFY', 'false').lower() != 'true'
|
|
},
|
|
'proxmox_2': {
|
|
'api_url': env_vars.get('PROXMOX_2_API_URL', 'https://192.168.11.11:8006'),
|
|
'user': env_vars.get('PROXMOX_2_USER', 'root'),
|
|
'password': env_vars.get('PROXMOX_2_PASS', ''),
|
|
'token': env_vars.get('PROXMOX_2_API_TOKEN', ''),
|
|
'verify_ssl': env_vars.get('PROXMOX_2_INSECURE_SKIP_TLS_VERIFY', 'false').lower() != 'true'
|
|
}
|
|
}
|
|
|
|
return config
|
|
|
|
def connect_and_review(client: ProxmoxClient, instance_num: int, output_dir: Path) -> Optional[dict]:
|
|
"""Connect to Proxmox and gather information."""
|
|
log(f"Connecting to Proxmox Instance {instance_num}...")
|
|
|
|
if not client.authenticate():
|
|
log_error(f"Failed to authenticate to Instance {instance_num}")
|
|
return None
|
|
|
|
log_success(f"Authenticated to Instance {instance_num}")
|
|
|
|
# Gather information
|
|
info = {
|
|
'instance': instance_num,
|
|
'timestamp': datetime.now().isoformat(),
|
|
'version': client.get_version(),
|
|
'cluster_status': client.get_cluster_status(),
|
|
'nodes': client.get_nodes(),
|
|
'storage': client.get_storage()
|
|
}
|
|
|
|
# Get detailed node information
|
|
if info['nodes']:
|
|
log(f" Found {len(info['nodes'])} nodes")
|
|
for node_data in info['nodes']:
|
|
node = node_data.get('node', 'unknown')
|
|
log(f" - {node}")
|
|
|
|
# Get node status
|
|
node_status = client.get_node_status(node)
|
|
if node_status:
|
|
info[f'node_{node}_status'] = node_status
|
|
|
|
# Get VMs
|
|
vms = client.get_vms(node)
|
|
if vms:
|
|
info[f'node_{node}_vms'] = vms
|
|
log(f" VMs: {len(vms)}")
|
|
|
|
# Get networks
|
|
networks = client.get_networks(node)
|
|
if networks:
|
|
info[f'node_{node}_networks'] = networks
|
|
|
|
# Save to file
|
|
output_file = output_dir / f"proxmox-{instance_num}-status-{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
with open(output_file, 'w') as f:
|
|
json.dump(info, f, indent=2)
|
|
|
|
log_success(f"Status saved to {output_file}")
|
|
|
|
# Display summary
|
|
if info.get('version'):
|
|
version = info['version'].get('version', 'unknown')
|
|
log(f" Version: {version}")
|
|
|
|
return info
|
|
|
|
def review_configurations(project_root: Path, output_dir: Path) -> str:
|
|
"""Review and document configurations."""
|
|
log("Reviewing configurations...")
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
config_file = output_dir / f"configuration-review-{timestamp}.md"
|
|
|
|
content = []
|
|
content.append("# Proxmox Configuration Review\n")
|
|
content.append(f"Generated: {datetime.now().isoformat()}\n")
|
|
|
|
# Environment configuration
|
|
content.append("## Environment Configuration\n")
|
|
content.append("### Proxmox Instance 1\n")
|
|
content.append("- API URL: From .env (PROXMOX_1_API_URL)\n")
|
|
content.append("- User: From .env (PROXMOX_1_USER)\n")
|
|
content.append("\n### Proxmox Instance 2\n")
|
|
content.append("- API URL: From .env (PROXMOX_2_API_URL)\n")
|
|
content.append("- User: From .env (PROXMOX_2_USER)\n")
|
|
content.append("\n")
|
|
|
|
# Provider config
|
|
provider_config = project_root / "crossplane-provider-proxmox" / "examples" / "provider-config.yaml"
|
|
if provider_config.exists():
|
|
content.append("## Crossplane Provider Configuration\n")
|
|
content.append("```yaml\n")
|
|
with open(provider_config) as f:
|
|
content.append(f.read())
|
|
content.append("```\n\n")
|
|
|
|
# Cloudflare tunnel configs
|
|
tunnel_configs_dir = project_root / "cloudflare" / "tunnel-configs"
|
|
if tunnel_configs_dir.exists():
|
|
content.append("## Cloudflare Tunnel Configurations\n")
|
|
for config_file in sorted(tunnel_configs_dir.glob("proxmox-site-*.yaml")):
|
|
content.append(f"### {config_file.name}\n")
|
|
content.append("```yaml\n")
|
|
with open(config_file) as f:
|
|
content.append(f.read())
|
|
content.append("```\n\n")
|
|
|
|
with open(config_file, 'w') as f:
|
|
f.write(''.join(content))
|
|
|
|
log_success(f"Configuration review saved to {config_file}")
|
|
return str(config_file)
|
|
|
|
def generate_deployment_plan(output_dir: Path, config: dict) -> str:
|
|
"""Generate deployment plan."""
|
|
log("Generating deployment plan...")
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
plan_file = output_dir / f"deployment-plan-{timestamp}.md"
|
|
|
|
content = []
|
|
content.append("# Proxmox Deployment Plan\n")
|
|
content.append(f"Generated: {datetime.now().isoformat()}\n")
|
|
content.append("## Current Status\n")
|
|
content.append(f"- **Instance 1**: {config['proxmox_1']['api_url']}\n")
|
|
content.append(f"- **Instance 2**: {config['proxmox_2']['api_url']}\n")
|
|
content.append("\n## Deployment Phases\n")
|
|
content.append("### Phase 1: Connection and Validation\n")
|
|
content.append("1. Verify connectivity to both instances\n")
|
|
content.append("2. Review cluster status and node health\n")
|
|
content.append("3. Review storage and network configuration\n")
|
|
content.append("\n### Phase 2: Configuration Alignment\n")
|
|
content.append("1. Map instances to sites\n")
|
|
content.append("2. Set up authentication (API tokens)\n")
|
|
content.append("3. Configure Cloudflare tunnels\n")
|
|
content.append("\n### Phase 3: Crossplane Provider Deployment\n")
|
|
content.append("1. Complete API client implementation\n")
|
|
content.append("2. Build and deploy provider\n")
|
|
content.append("3. Configure ProviderConfig\n")
|
|
content.append("\n### Phase 4: Infrastructure Deployment\n")
|
|
content.append("1. Deploy test VMs\n")
|
|
content.append("2. Set up monitoring\n")
|
|
content.append("3. Configure backups\n")
|
|
content.append("\n### Phase 5: Production Readiness\n")
|
|
content.append("1. Security hardening\n")
|
|
content.append("2. Documentation\n")
|
|
content.append("3. Testing and validation\n")
|
|
|
|
with open(plan_file, 'w') as f:
|
|
f.write(''.join(content))
|
|
|
|
log_success(f"Deployment plan saved to {plan_file}")
|
|
return str(plan_file)
|
|
|
|
def generate_task_list(output_dir: Path, config: dict) -> str:
|
|
"""Generate detailed task list."""
|
|
log("Generating task list...")
|
|
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
task_file = output_dir / f"task-list-{timestamp}.md"
|
|
|
|
content = []
|
|
content.append("# Proxmox Deployment Task List\n")
|
|
content.append(f"Generated: {datetime.now().isoformat()}\n")
|
|
content.append("## Immediate Tasks (Priority: High)\n")
|
|
content.append("### Connection and Authentication\n")
|
|
content.append("- [ ] **TASK-001**: Verify connectivity to Instance 1\n")
|
|
content.append(f" - URL: {config['proxmox_1']['api_url']}\n")
|
|
content.append("- [ ] **TASK-002**: Verify connectivity to Instance 2\n")
|
|
content.append(f" - URL: {config['proxmox_2']['api_url']}\n")
|
|
content.append("- [ ] **TASK-003**: Test authentication to Instance 1\n")
|
|
content.append("- [ ] **TASK-004**: Test authentication to Instance 2\n")
|
|
content.append("\n### Configuration Review\n")
|
|
content.append("- [ ] **TASK-005**: Review provider-config.yaml\n")
|
|
content.append("- [ ] **TASK-006**: Review Cloudflare tunnel configs\n")
|
|
content.append("- [ ] **TASK-007**: Map instances to sites\n")
|
|
content.append("\n## Short-term Tasks (Priority: Medium)\n")
|
|
content.append("### Crossplane Provider\n")
|
|
content.append("- [ ] **TASK-008**: Complete Proxmox API client implementation\n")
|
|
content.append("- [ ] **TASK-009**: Build and test provider\n")
|
|
content.append("- [ ] **TASK-010**: Deploy provider to Kubernetes\n")
|
|
content.append("- [ ] **TASK-011**: Create ProviderConfig resource\n")
|
|
content.append("\n### Infrastructure Setup\n")
|
|
content.append("- [ ] **TASK-012**: Deploy Prometheus exporters\n")
|
|
content.append("- [ ] **TASK-013**: Configure Cloudflare tunnels\n")
|
|
content.append("- [ ] **TASK-014**: Set up monitoring dashboards\n")
|
|
content.append("\n## Long-term Tasks (Priority: Low)\n")
|
|
content.append("- [ ] **TASK-015**: Deploy test VMs\n")
|
|
content.append("- [ ] **TASK-016**: End-to-end testing\n")
|
|
content.append("- [ ] **TASK-017**: Performance testing\n")
|
|
content.append("- [ ] **TASK-018**: Create runbooks\n")
|
|
content.append("- [ ] **TASK-019**: Set up backups\n")
|
|
content.append("- [ ] **TASK-020**: Security audit\n")
|
|
|
|
with open(task_file, 'w') as f:
|
|
f.write(''.join(content))
|
|
|
|
log_success(f"Task list saved to {task_file}")
|
|
return str(task_file)
|
|
|
|
def main():
|
|
"""Main execution."""
|
|
log("Starting Proxmox Review and Deployment Planning...")
|
|
log("=" * 50)
|
|
|
|
project_root = Path(__file__).parent.parent
|
|
output_dir = project_root / "docs" / "proxmox-review"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
config = load_environment()
|
|
|
|
log("\n=== Phase 1: Connecting to Proxmox Instances ===")
|
|
|
|
# Connect to Instance 1
|
|
client1 = ProxmoxClient(
|
|
config['proxmox_1']['api_url'],
|
|
config['proxmox_1']['user'],
|
|
config['proxmox_1']['password'],
|
|
config['proxmox_1']['token'],
|
|
config['proxmox_1']['verify_ssl']
|
|
)
|
|
info1 = connect_and_review(client1, 1, output_dir)
|
|
|
|
log("")
|
|
|
|
# Connect to Instance 2
|
|
client2 = ProxmoxClient(
|
|
config['proxmox_2']['api_url'],
|
|
config['proxmox_2']['user'],
|
|
config['proxmox_2']['password'],
|
|
config['proxmox_2']['token'],
|
|
config['proxmox_2']['verify_ssl']
|
|
)
|
|
info2 = connect_and_review(client2, 2, output_dir)
|
|
|
|
log("\n=== Phase 2: Reviewing Configurations ===")
|
|
review_configurations(project_root, output_dir)
|
|
|
|
log("\n=== Phase 3: Generating Deployment Plan ===")
|
|
generate_deployment_plan(output_dir, config)
|
|
|
|
log("\n=== Phase 4: Generating Task List ===")
|
|
generate_task_list(output_dir, config)
|
|
|
|
log("\n" + "=" * 50)
|
|
log_success("Review and planning completed!")
|
|
log(f"\nOutput directory: {output_dir}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|