All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 30s
291 lines
10 KiB
Python
291 lines
10 KiB
Python
import os
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import shutil
|
|
import time
|
|
from django.conf import settings
|
|
from django.db import connections
|
|
from django.http import JsonResponse
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from crm.permissions import IsCRMAdmin
|
|
|
|
SERVICES = {
|
|
'crm': {
|
|
'name': 'CRM Backend',
|
|
'dir': '/home/vignesh/github/crmbackend',
|
|
'venv_python': '/home/vignesh/github/crmbackend/.venv/bin/python',
|
|
'venv_gunicorn': '/home/vignesh/github/crmbackend/.venv/bin/gunicorn',
|
|
'wsgi': 'digihoxbackend.wsgi:application',
|
|
'port': 8081,
|
|
'logs_dir': '/home/vignesh/github/crmbackend/logs',
|
|
},
|
|
'seller': {
|
|
'name': 'Seller Central Backend',
|
|
'dir': '/home/vignesh/github/seller_central_backend',
|
|
'venv_python': '/home/vignesh/github/seller_central_backend/venv/bin/python',
|
|
'venv_gunicorn': '/home/vignesh/github/seller_central_backend/venv/bin/gunicorn',
|
|
'wsgi': 'config.wsgi:application',
|
|
'port': 8080,
|
|
'logs_dir': '/home/vignesh/github/seller_central_backend/logs',
|
|
},
|
|
'customer': {
|
|
'name': 'Customer Website Backend',
|
|
'dir': '/home/vignesh/github/customerwebsitebackend',
|
|
'venv_python': '/home/vignesh/github/customerwebsitebackend/venv/bin/python',
|
|
'venv_gunicorn': '/home/vignesh/github/customerwebsitebackend/venv/bin/gunicorn',
|
|
'wsgi': 'customerwebsitebackend.wsgi:application',
|
|
'port': 8082,
|
|
'logs_dir': '/home/vignesh/github/customerwebsitebackend/logs',
|
|
}
|
|
}
|
|
|
|
def is_port_open(port):
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(0.5)
|
|
return s.connect_ex(('127.0.0.1', port)) == 0
|
|
|
|
def find_pids_for_service(service_key):
|
|
cfg = SERVICES.get(service_key)
|
|
if not cfg:
|
|
return []
|
|
pids = []
|
|
# Search via ps command for any gunicorn or python processes running in that directory
|
|
try:
|
|
cmd = f"ps -eo pid,cmd | grep '{cfg['dir']}' | grep -v grep"
|
|
out = subprocess.check_output(cmd, shell=True, text=True)
|
|
for line in out.strip().split('\n'):
|
|
parts = line.strip().split(None, 1)
|
|
if len(parts) >= 1:
|
|
pids.append(int(parts[0]))
|
|
except subprocess.CalledProcessError:
|
|
pass
|
|
return pids
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsCRMAdmin])
|
|
def service_status(request):
|
|
results = {}
|
|
for key, cfg in SERVICES.items():
|
|
pids = find_pids_for_service(key)
|
|
port_active = is_port_open(cfg['port'])
|
|
results[key] = {
|
|
'name': cfg['name'],
|
|
'directory': cfg['dir'],
|
|
'port': cfg['port'],
|
|
'port_active': port_active,
|
|
'running': len(pids) > 0 or port_active,
|
|
'pids': pids
|
|
}
|
|
return JsonResponse(results)
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsCRMAdmin])
|
|
def service_control(request):
|
|
service_key = request.data.get('service')
|
|
action = request.data.get('action') # start, stop, restart
|
|
|
|
if service_key not in SERVICES:
|
|
return JsonResponse({'error': 'Invalid service. Choose from crm, seller, customer'}, status=400)
|
|
if action not in ['start', 'stop', 'restart']:
|
|
return JsonResponse({'error': 'Invalid action. Choose from start, stop, restart'}, status=400)
|
|
|
|
cfg = SERVICES[service_key]
|
|
|
|
# 1. Stop if running or restart
|
|
if action in ['stop', 'restart']:
|
|
pids = find_pids_for_service(service_key)
|
|
for pid in pids:
|
|
try:
|
|
os.kill(pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
time.sleep(1)
|
|
# Verify if still alive, then SIGKILL
|
|
pids = find_pids_for_service(service_key)
|
|
for pid in pids:
|
|
try:
|
|
os.kill(pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
if action == 'stop':
|
|
return JsonResponse({'message': f'Stopped {cfg["name"]}'})
|
|
|
|
# 2. Start (or restart continuation)
|
|
# Ensure logs folder exists
|
|
os.makedirs(cfg['logs_dir'], exist_ok=True)
|
|
|
|
# We will launch using gunicorn if present, else fallback to manage.py runserver
|
|
if os.path.exists(cfg['venv_gunicorn']):
|
|
cmd = [
|
|
cfg['venv_gunicorn'],
|
|
cfg['wsgi'],
|
|
'--bind', f'0.0.0.0:{cfg["port"]}',
|
|
'--workers', '2',
|
|
'--access-logfile', os.path.join(cfg['logs_dir'], 'gunicorn-access.log'),
|
|
'--error-logfile', os.path.join(cfg['logs_dir'], 'gunicorn-error.log'),
|
|
]
|
|
else:
|
|
cmd = [
|
|
cfg['venv_python'],
|
|
os.path.join(cfg['dir'], 'manage.py'),
|
|
'runserver',
|
|
f'0.0.0.0:{cfg["port"]}'
|
|
]
|
|
|
|
# Run in background session
|
|
try:
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=cfg['dir'],
|
|
stdout=open(os.path.join(cfg['logs_dir'], 'stdout.log'), 'a'),
|
|
stderr=open(os.path.join(cfg['logs_dir'], 'stderr.log'), 'a'),
|
|
start_new_session=True
|
|
)
|
|
return JsonResponse({
|
|
'message': f'Started {cfg["name"]}',
|
|
'pid': proc.pid,
|
|
'port': cfg['port']
|
|
})
|
|
except Exception as e:
|
|
return JsonResponse({'error': f'Failed to start: {str(e)}'}, status=500)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsCRMAdmin])
|
|
def list_and_search_logs(request):
|
|
service_key = request.query_params.get('service')
|
|
log_file_name = request.query_params.get('log_file')
|
|
lines_count = int(request.query_params.get('lines', 100))
|
|
search_query = request.query_params.get('search', '').lower()
|
|
|
|
if service_key not in SERVICES:
|
|
return JsonResponse({'error': 'Invalid service'}, status=400)
|
|
|
|
cfg = SERVICES[service_key]
|
|
logs_dir = cfg['logs_dir']
|
|
|
|
if not os.path.exists(logs_dir):
|
|
return JsonResponse({'message': 'No logs directory found', 'files': []})
|
|
|
|
available_files = [f for f in os.listdir(logs_dir) if os.path.isfile(os.path.join(logs_dir, f))]
|
|
|
|
if not log_file_name:
|
|
return JsonResponse({
|
|
'service': service_key,
|
|
'logs_dir': logs_dir,
|
|
'available_files': available_files
|
|
})
|
|
|
|
if log_file_name not in available_files:
|
|
return JsonResponse({'error': f'Log file not found. Available: {available_files}'}, status=404)
|
|
|
|
filepath = os.path.join(logs_dir, log_file_name)
|
|
try:
|
|
# Read file lines from the end
|
|
with open(filepath, 'r', errors='ignore') as f:
|
|
lines = f.readlines()
|
|
|
|
# Filter if search query is provided
|
|
if search_query:
|
|
lines = [l for l in lines if search_query in l.lower()]
|
|
|
|
# Slice the last lines
|
|
selected_lines = lines[-lines_count:]
|
|
|
|
return JsonResponse({
|
|
'file': log_file_name,
|
|
'total_lines_found': len(lines),
|
|
'returned_lines_count': len(selected_lines),
|
|
'content': ''.join(selected_lines)
|
|
})
|
|
except Exception as e:
|
|
return JsonResponse({'error': f'Failed to read log: {str(e)}'}, status=500)
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([IsCRMAdmin])
|
|
def system_diagnostics(request):
|
|
# 1. System stats
|
|
total, used, free = shutil.disk_usage("/")
|
|
sys_stats = {
|
|
'disk': {
|
|
'total_gb': total // (2**30),
|
|
'used_gb': used // (2**30),
|
|
'free_gb': free // (2**30),
|
|
'percent_used': round((used / total) * 100, 2)
|
|
}
|
|
}
|
|
|
|
# Try reading /proc/meminfo for memory stats
|
|
try:
|
|
with open('/proc/meminfo', 'r') as f:
|
|
lines = f.readlines()
|
|
mem_total = 0
|
|
mem_free = 0
|
|
mem_avail = 0
|
|
for line in lines:
|
|
if 'MemTotal' in line:
|
|
mem_total = int(line.split()[1])
|
|
elif 'MemFree' in line:
|
|
mem_free = int(line.split()[1])
|
|
elif 'MemAvailable' in line:
|
|
mem_avail = int(line.split()[1])
|
|
if mem_total > 0:
|
|
sys_stats['memory'] = {
|
|
'total_mb': mem_total // 1024,
|
|
'available_mb': mem_avail // 1024 if mem_avail else mem_free // 1024,
|
|
'percent_used': round(((mem_total - (mem_avail if mem_avail else mem_free)) / mem_total) * 100, 2)
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
# 2. Database checks
|
|
db_status = {}
|
|
for db_alias in connections:
|
|
try:
|
|
connections[db_alias].ensure_connection()
|
|
db_status[db_alias] = 'connected'
|
|
except Exception as e:
|
|
db_status[db_alias] = f'failed: {str(e)}'
|
|
|
|
return JsonResponse({
|
|
'system_resources': sys_stats,
|
|
'database_connections': db_status
|
|
})
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([IsCRMAdmin])
|
|
def run_management_command(request):
|
|
service_key = request.data.get('service')
|
|
command = request.data.get('command') # e.g. check, showmigrations, migrate
|
|
|
|
if service_key not in SERVICES:
|
|
return JsonResponse({'error': 'Invalid service'}, status=400)
|
|
|
|
# Basic protection against arbitrary CLI injection
|
|
allowed_commands = ['check', 'showmigrations', 'migrate', 'test']
|
|
base_cmd = command.split()[0] if command else ''
|
|
if base_cmd not in allowed_commands:
|
|
return JsonResponse({'error': f'Command not allowed. Allowed list: {allowed_commands}'}, status=400)
|
|
|
|
cfg = SERVICES[service_key]
|
|
full_cmd = [cfg['venv_python'], 'manage.py'] + command.split()
|
|
|
|
try:
|
|
res = subprocess.run(
|
|
full_cmd,
|
|
cwd=cfg['dir'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30
|
|
)
|
|
return JsonResponse({
|
|
'command': f'python manage.py {command}',
|
|
'exit_code': res.returncode,
|
|
'stdout': res.stdout,
|
|
'stderr': res.stderr
|
|
})
|
|
except subprocess.TimeoutExpired:
|
|
return JsonResponse({'error': 'Command timed out after 30 seconds'}, status=504)
|
|
except Exception as e:
|
|
return JsonResponse({'error': f'Execution failed: {str(e)}'}, status=500)
|