This commit is contained in:
Your Name
2026-04-11 18:12:42 -03:00
parent b14d2f4fc1
commit 73090ddaa1
12 changed files with 346 additions and 128 deletions
+183 -83
View File
@@ -1,30 +1,27 @@
#!/usr/bin/env python3
from fastapi import FastAPI, HTTPException, Depends, Response
from fastapi.security.api_key import APIKeyHeader
from fastapi.responses import RedirectResponse, PlainTextResponse
from pydantic import BaseModel
import psutil
import json
import os
import re
import sqlite3
import subprocess
import os
import uvicorn
from typing import Dict, Any, Optional
from datetime import datetime
import json
import re
from collections import deque
import time
from collections import deque
from datetime import datetime
from pathlib import Path
from pathlib import Path
from typing import Any, Dict, Optional
import psutil
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Response
from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
# ---------------------- Constants ----------------------
DB_PATH = "/4server/data/contracts.db"
BIN_PATH = "/4server/sbin"
API_KEY = os.getenv("API_KEY", "your-secret-api-key")
VERSION = "API: 0.0.8"
VERSION = "API: 0.0.9"
# ---------------------- FastAPI App ----------------------
app = FastAPI()
@@ -57,7 +54,7 @@ def init_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
cursor.execute("""
CREATE TABLE IF NOT EXISTS containers (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID CHAR(50) UNIQUE,
@@ -74,9 +71,19 @@ def init_db():
created DATE,
bump DATE,
secret TEXT,
contract TEXT
contract TEXT,
git integer,
backup_slots integer,
hdd integer,
workers integer,
utm_source text,
utm_medium text,
utm_campaign text,
domains_slots integer,
secret_list TEXT,
env_list TEXT
)
''')
""")
conn.commit()
conn.close()
@@ -101,13 +108,12 @@ class ContractItem(BaseModel):
product_id: int
features: Dict[str, Any]
class ContainerModel(BaseModel):
UUID: str
email: Optional[str] = None
expires: Optional[str] = None
tags: Optional[str] = None
env: Optional[Dict[str, Any]] = None
affiliate: Optional[str] = None
image: Optional[str] = None
history: Optional[str] = None
comment: Optional[str] = None
@@ -115,37 +121,56 @@ class ContainerModel(BaseModel):
status: Optional[str] = None
created: Optional[str] = None
bump: Optional[str] = None
secret: Optional[Dict[str, Any]] = None
contract: Optional[str] = None
git: Optional[int] = None
backup_slots: Optional[int] = None
hdd: Optional[int] = None
workers: Optional[int] = None
utm_source: Optional[str] = None
utm_medium: Optional[str] = None
utm_campaign: Optional[str] = None
domains_slots: Optional[int] = None
env_list: Optional[str] = None
class UUIDRequest(BaseModel):
UUID: str
class CommandRequest(BaseModel):
uuid: str
method: int
method: int
class ImportRequest(BaseModel):
filename: str
class MoveRequest(BaseModel):
source: str
destination: str
class AddKeyRequest(BaseModel):
key: str
uuid: str
class ImageRequest(BaseModel):
image: str
# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
return RedirectResponse(url="https://ODOO4PROJECTS.com")
from fastapi import FastAPI, Depends
import json
from typing import Any, Dict, Optional
from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from typing import Optional, Dict, Any
import json
app = FastAPI()
@@ -158,25 +183,42 @@ def redirect_to_odoo():
@app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel):
# Convert dict fields to JSON strings
env_str = json.dumps(request.env) if isinstance(request.env, dict) else None
secret_str = json.dumps(request.secret) if isinstance(request.secret, dict) else None
contract_str = json.dumps(request.contract) if isinstance(request.contract, dict) else None
# Fetch existing record
existing = execute_db("SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True)
existing = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
)
if not existing:
# If record does not exist, insert a new one with all given fields
execute_db("""
INSERT INTO containers (UUID, email, expires, tags, env, affiliate, image, history,
comment, domains, status, created, bump, secret, contract)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
request.UUID, request.email, request.expires, request.tags, env_str,
request.affiliate, request.image, request.history, request.comment,
request.domains, request.status, request.created, request.bump, secret_str, contract_str
))
execute_db(
"""
INSERT INTO containers (UUID, email, expires, tags, image, history,
comment, domains, status, created, bump, git, backup_slots, hdd, workers,
utm_source, utm_medium, utm_campaign, domains_slots, env_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
request.UUID,
request.email,
request.expires,
request.tags,
request.image,
request.history,
request.comment,
request.domains,
request.status,
request.created,
request.bump,
request.git,
request.backup_slots,
request.hdd,
request.workers,
request.utm_source,
request.utm_medium,
request.utm_campaign,
request.domains_slots,
request.env_list,
),
)
return {"UUID": request.UUID, "status": "created"}
# Existing record found, do partial update
@@ -190,9 +232,6 @@ def update_container(request: ContainerModel):
continue
value = getattr(request, field)
if value is not None:
if field in ["env", "secret", "contract"]:
value = json.dumps(value)
updates[field] = value
params.append(value)
@@ -201,7 +240,11 @@ def update_container(request: ContainerModel):
set_clause = ", ".join(f"{k}=?" for k in updates.keys())
params.append(request.UUID) # UUID for WHERE clause
execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params))
return {"UUID": request.UUID, "status": "updated", "fields_updated": list(updates.keys())}
return {
"UUID": request.UUID,
"status": "updated",
"fields_updated": list(updates.keys()),
}
return {"UUID": request.UUID, "status": "no_change"}
@@ -211,6 +254,11 @@ def start_container(request: UUIDRequest):
return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])}
@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)])
def start_container(request: ImageRequest):
return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@app.post("/container/stop", dependencies=[Depends(verify_api_key)])
def stop_container(request: UUIDRequest):
try:
@@ -222,7 +270,9 @@ def stop_container(request: UUIDRequest):
@app.post("/container/nuke", dependencies=[Depends(verify_api_key)])
def nuke_container(request: UUIDRequest):
status = execute_db("SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True)
status = execute_db(
"SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True
)
if not status or status[0]["status"] != "nuke":
raise HTTPException(400, "Container status is not 'nuke'")
return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])}
@@ -232,8 +282,26 @@ def nuke_container(request: UUIDRequest):
def info_container(request: Optional[UUIDRequest] = None):
# Fields to select
fields = [
"ID", "UUID", "email", "expires", "tags", "env", "affiliate",
"image", "history", "comment", "domains", "status", "created", "contract"
"ID",
"UUID",
"email",
"expires",
"tags",
"image",
"history",
"comment",
"domains",
"status",
"created",
"git",
"backup_slots",
"hdd",
"workers",
"utm_source",
"utm_medium",
"utm_campaign",
"domains_slots",
"env_list",
]
field_str = ", ".join(fields)
@@ -242,13 +310,10 @@ def info_container(request: Optional[UUIDRequest] = None):
rows = execute_db(
f"SELECT {field_str} FROM containers WHERE UUID=?",
(str(request.UUID),),
fetch=True
fetch=True,
)
else:
rows = execute_db(
f"SELECT {field_str} FROM containers",
fetch=True
)
rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True)
# Map rows to dicts safely
containers = []
@@ -265,6 +330,7 @@ def info_container(request: Optional[UUIDRequest] = None):
return n8n_items
@app.post("/container/bump", dependencies=[Depends(verify_api_key)])
def bump_container(request: UUIDRequest):
today = datetime.utcnow().strftime("%Y-%m-%d")
@@ -275,10 +341,16 @@ def bump_container(request: UUIDRequest):
@app.post("/container/quota", dependencies=[Depends(verify_api_key)])
def container_quota(request: UUIDRequest):
output = run_command([
"docker", "stats", request.UUID, "--no-stream",
"--format", "{{.MemUsage}},{{.BlockIO}}"
])
output = run_command(
[
"docker",
"stats",
request.UUID,
"--no-stream",
"--format",
"{{.MemUsage}},{{.BlockIO}}",
]
)
mem_usage, disk_usage = output.split(",")
return {"memory_usage": mem_usage, "disk_io": disk_usage}
@@ -286,7 +358,10 @@ def container_quota(request: UUIDRequest):
# ---------------------- SYSTEM ----------------------
@app.get("/system/containers", dependencies=[Depends(verify_api_key)])
def get_containers():
return Response(content=run_command([f"{BIN_PATH}/getContainers"]), media_type="application/json")
return Response(
content=run_command([f"{BIN_PATH}/getContainers"]),
media_type="application/json",
)
@app.get("/system/images", dependencies=[Depends(verify_api_key)])
@@ -294,12 +369,13 @@ def list_images():
images = run_command(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"])
return {"images": images.split("\n")}
@app.get("/system/cpu", dependencies=[Depends(verify_api_key)])
def get_cpu_log():
CPU_LOG_PATH = Path("/4server/data/log/cpu.log")
if not CPU_LOG_PATH.exists():
raise HTTPException(status_code=404, detail="CPU log file not found")
try:
with CPU_LOG_PATH.open("r") as f:
content = f.read()
@@ -307,12 +383,15 @@ def get_cpu_log():
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading CPU log: {e}")
@app.get("/system/info", dependencies=[Depends(verify_api_key)])
def get_system_info():
try:
alpine_version = None
last_update = None
bump_dates = execute_db("SELECT MAX(bump) AS latest_bump FROM containers", fetch=True)[0]["latest_bump"]
bump_dates = execute_db(
"SELECT MAX(bump) AS latest_bump FROM containers", fetch=True
)[0]["latest_bump"]
if os.path.exists("/4server/data/update"):
with open("/4server/data/update") as f:
last_update = f.read().strip()
@@ -328,10 +407,14 @@ def get_system_info():
"latest_bump": bump_dates,
"version": VERSION,
"resources": {
"memory": {"total": mem.total, "available": mem.available, "used": mem.used},
"memory": {
"total": mem.total,
"available": mem.available,
"used": mem.used,
},
"disk": {"total": disk.total, "used": disk.used, "free": disk.free},
"cpu_count": cpu_count
}
"cpu_count": cpu_count,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -342,6 +425,13 @@ def pull_all_images():
return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])}
@app.post("/system/restartAllContainers", dependencies=[Depends(verify_api_key)])
def restart_all_containers(request: ImageRequest):
if not request.image:
raise HTTPException(status_code=400, detail="Image name is required")
return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@app.post("/client/git", dependencies=[Depends(verify_api_key)])
def git_tool(request: CommandRequest):
if request.method == 1:
@@ -359,7 +449,7 @@ def git_tool(request: CommandRequest):
def add_git_key(request: AddKeyRequest):
if not request.key or not request.uuid:
raise HTTPException(status_code=400, detail="Key and uuid are required")
file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub"
try:
@@ -370,7 +460,7 @@ def add_git_key(request: AddKeyRequest):
f.write(request.key)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}")
return {"message": f"Key saved for container {request.uuid}", "file": file_path}
@@ -382,7 +472,10 @@ def audit_odoo(uuid: str):
@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)])
async def get_odoo_log_summary(uuid: str):
if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid):
raise HTTPException(status_code=400, detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.")
raise HTTPException(
status_code=400,
detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.",
)
BASE_LOG_DIR = "/4server/data"
@@ -396,12 +489,12 @@ async def get_odoo_log_summary(uuid: str):
# --- Helper variables and functions ---
IMPORTANT_PATTERNS = [
re.compile(r"odoo\.addons\."), # Any Odoo addon log
re.compile(r"Job '.*' starting"), # Cron job start
re.compile(r"Job '.*' fully done"), # Cron job end
re.compile(r"ERROR"), # Errors
re.compile(r"WARNING"), # Warnings
re.compile(r"Traceback"), # Tracebacks
re.compile(r"odoo\.addons\."), # Any Odoo addon log
re.compile(r"Job '.*' starting"), # Cron job start
re.compile(r"Job '.*' fully done"), # Cron job end
re.compile(r"ERROR"), # Errors
re.compile(r"WARNING"), # Warnings
re.compile(r"Traceback"), # Tracebacks
re.compile(r"Error"),
]
@@ -424,7 +517,9 @@ async def get_odoo_log_summary(uuid: str):
try:
# Last 500 lines from Odoo log
last_500_lines = read_last_lines(odoo_log_file, 500)
important_odoo_lines = [line for line in last_500_lines if is_important_line(line)]
important_odoo_lines = [
line for line in last_500_lines if is_important_line(line)
]
# Last 50 lines from git.log
last_50_git_lines = read_last_lines(git_log_file, 50)
@@ -437,30 +532,37 @@ async def get_odoo_log_summary(uuid: str):
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading log files: {e}")
# ------------------------ BACKUP HANDLING -------------------------------------
@app.post("/backup/import", dependencies=[Depends(verify_api_key)])
def backup_import(request: ImportRequest):
if not request.filename:
raise HTTPException(status_code=400, detail="Filename is required")
command = [f"{BIN_PATH}/ODOO_19/import", request.filename]
output = run_command(command)
return {"message": output}
@app.post("/backup/move", dependencies=[Depends(verify_api_key)])
def backup_move(request: MoveRequest):
if not request.source or not request.destination:
raise HTTPException(status_code=400, detail="Source and destination are required")
raise HTTPException(
status_code=400, detail="Source and destination are required"
)
if not os.path.exists(request.source):
raise HTTPException(status_code=404, detail="Source file does not exist")
# Use shell command to move the file
command = ["mv", request.source, request.destination] # Linux/macOS
# For Windows, use: command = ["move", request.source, request.destination]
output = run_command(command)
return {"message": f"Moved {request.source} to {request.destination}", "output": output}
return {
"message": f"Moved {request.source} to {request.destination}",
"output": output,
}
# ---------------------- Entry Point ----------------------
@@ -469,5 +571,3 @@ if __name__ == "__main__":
init_db()
time.sleep(25)
uvicorn.run(app, host="10.5.0.1", port=8888)