feat: export/import configuration and multiple bug fixes
- Add export/import endpoints for full config backup (OCI, GenAI, MCP, prompts, settings, ADB) - Export includes private keys (base64) for complete environment migration - Import merges by ID (skips existing, no duplicates) - Fix Resource Plan parser to recognize free-form format (model skips ```plan blocks) - Fix replan not clearing previous apply/destroy outputs - Fix duplicate CIS Compliance Scanner MCP server on multi-worker startup - Add Terraform prompt editor with auto-save in menu
This commit is contained in:
129
backend/app.py
129
backend/app.py
@@ -17,7 +17,7 @@ from fastapi import (
|
||||
Query, Body, BackgroundTasks
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.responses import JSONResponse, FileResponse, StreamingResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel
|
||||
import jwt as pyjwt
|
||||
@@ -4336,6 +4336,133 @@ async def delete_prompt(pid: str, u=Depends(require("admin"))):
|
||||
c.execute("DELETE FROM system_prompts WHERE id=?", (pid,))
|
||||
return {"ok": True}
|
||||
|
||||
# ── Export / Import Configuration ─────────────────────────────────────────────
|
||||
@app.get("/api/config/export")
|
||||
async def export_config(u=Depends(require("admin"))):
|
||||
"""Export all configurations as JSON (OCI configs with keys, GenAI, MCP, prompts, settings)."""
|
||||
data = {"version": VERSION, "exported_at": datetime.now().isoformat()}
|
||||
with db() as c:
|
||||
# OCI configs
|
||||
oci_rows = c.execute("SELECT * FROM oci_configs").fetchall()
|
||||
oci_cfgs = []
|
||||
for r in oci_rows:
|
||||
cfg = dict(r)
|
||||
# Include private key content (base64)
|
||||
kp = Path(cfg.get("key_file_path", ""))
|
||||
if kp.exists():
|
||||
cfg["_private_key_b64"] = base64.b64encode(kp.read_bytes()).decode()
|
||||
# Include passphrase from config file if exists
|
||||
cfg_file = kp.parent / "config" if kp.parent.exists() else None
|
||||
if cfg_file and cfg_file.exists():
|
||||
for line in cfg_file.read_text().splitlines():
|
||||
if line.startswith("pass_phrase="):
|
||||
cfg["_key_passphrase"] = line.split("=", 1)[1]
|
||||
oci_cfgs.append(cfg)
|
||||
data["oci_configs"] = oci_cfgs
|
||||
# GenAI configs
|
||||
data["genai_configs"] = [dict(r) for r in c.execute("SELECT * FROM genai_configs").fetchall()]
|
||||
# MCP servers (exclude system-auto-registered if tools are empty)
|
||||
data["mcp_servers"] = [dict(r) for r in c.execute("SELECT * FROM mcp_servers").fetchall()]
|
||||
# System prompts
|
||||
data["system_prompts"] = [dict(r) for r in c.execute("SELECT * FROM system_prompts").fetchall()]
|
||||
# App settings
|
||||
data["app_settings"] = [dict(r) for r in c.execute("SELECT * FROM app_settings").fetchall()]
|
||||
# ADB vector configs
|
||||
adb_rows = c.execute("SELECT * FROM adb_vector_configs").fetchall()
|
||||
data["adb_vector_configs"] = [dict(r) for r in adb_rows]
|
||||
# ADB vector tables
|
||||
data["adb_vector_tables"] = [dict(r) for r in c.execute("SELECT * FROM adb_vector_tables").fetchall()]
|
||||
return StreamingResponse(
|
||||
iter([json.dumps(data, indent=2, ensure_ascii=False)]),
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f'attachment; filename="oci-cis-agent-config-{datetime.now().strftime("%Y%m%d-%H%M%S")}.json"'}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/config/import")
|
||||
async def import_config(file: UploadFile = File(...), u=Depends(require("admin"))):
|
||||
"""Import configurations from exported JSON. Merges by ID (skip existing)."""
|
||||
content = await file.read()
|
||||
try:
|
||||
data = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(400, "Arquivo JSON inválido")
|
||||
counts = {"oci_configs": 0, "genai_configs": 0, "mcp_servers": 0, "system_prompts": 0, "app_settings": 0, "adb_vector_configs": 0, "adb_vector_tables": 0, "skipped": 0}
|
||||
with db() as c:
|
||||
# OCI configs
|
||||
for cfg in data.get("oci_configs", []):
|
||||
if c.execute("SELECT 1 FROM oci_configs WHERE id=?", (cfg["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
# Restore private key file
|
||||
cdir = OCI_DIR / cfg["id"]; cdir.mkdir(parents=True, exist_ok=True)
|
||||
kp = cdir / "oci_api_key.pem"
|
||||
if cfg.get("_private_key_b64"):
|
||||
kp.write_bytes(base64.b64decode(cfg["_private_key_b64"])); kp.chmod(0o600)
|
||||
# Write config file
|
||||
cfg_file = cdir / "config"
|
||||
cfg_content = (f"[DEFAULT]\nuser={_dec(cfg['user_ocid']) if cfg.get('user_ocid') else ''}\n"
|
||||
f"fingerprint={_dec(cfg['fingerprint']) if cfg.get('fingerprint') else ''}\n"
|
||||
f"tenancy={_dec(cfg['tenancy_ocid']) if cfg.get('tenancy_ocid') else ''}\n"
|
||||
f"region={cfg.get('region','')}\nkey_file={kp}\n")
|
||||
if cfg.get("_key_passphrase"):
|
||||
cfg_content += f"pass_phrase={cfg['_key_passphrase']}\n"
|
||||
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
|
||||
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("tenancy_name",""), cfg.get("tenancy_ocid",""), cfg.get("user_ocid",""),
|
||||
cfg.get("fingerprint",""), cfg.get("region",""), str(kp), cfg.get("compartment_id"), cfg.get("created_at")))
|
||||
counts["oci_configs"] += 1
|
||||
# GenAI configs
|
||||
for cfg in data.get("genai_configs", []):
|
||||
if c.execute("SELECT 1 FROM genai_configs WHERE id=?", (cfg["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
c.execute("INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k,frequency_penalty,presence_penalty,is_default,system_prompt,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("name",""), cfg.get("oci_config_id",""), cfg.get("model_id",""),
|
||||
cfg.get("model_ocid"), cfg.get("compartment_id",""), cfg.get("genai_region",""), cfg.get("endpoint",""),
|
||||
cfg.get("serving_type","ON_DEMAND"), cfg.get("dedicated_endpoint_id"), cfg.get("temperature",1),
|
||||
cfg.get("max_tokens",6000), cfg.get("top_p",0.95), cfg.get("top_k",1), cfg.get("frequency_penalty",0),
|
||||
cfg.get("presence_penalty",0), cfg.get("is_default",0), cfg.get("system_prompt",""), cfg.get("created_at")))
|
||||
counts["genai_configs"] += 1
|
||||
# MCP servers
|
||||
for srv in data.get("mcp_servers", []):
|
||||
if c.execute("SELECT 1 FROM mcp_servers WHERE id=?", (srv["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
c.execute("INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id,is_active,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(srv["id"], srv.get("user_id", u["id"]), srv.get("name",""), srv.get("description"), srv.get("server_type","stdio"),
|
||||
srv.get("command"), srv.get("args"), srv.get("env_vars"), srv.get("url"), srv.get("module_path"),
|
||||
srv.get("tools"), srv.get("linked_adb_id"), srv.get("is_active",1), srv.get("created_at")))
|
||||
counts["mcp_servers"] += 1
|
||||
# System prompts
|
||||
for p in data.get("system_prompts", []):
|
||||
if c.execute("SELECT 1 FROM system_prompts WHERE id=?", (p["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
c.execute("INSERT INTO system_prompts (id,name,agent,content,is_active,created_at) VALUES (?,?,?,?,?,?)",
|
||||
(p["id"], p.get("name",""), p.get("agent","chat"), p.get("content",""), p.get("is_active",0), p.get("created_at")))
|
||||
counts["system_prompts"] += 1
|
||||
# App settings
|
||||
for s in data.get("app_settings", []):
|
||||
c.execute("INSERT INTO app_settings (key,value,updated_at) VALUES (?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
|
||||
(s["key"], s.get("value",""), s.get("updated_at")))
|
||||
counts["app_settings"] += 1
|
||||
# ADB vector configs
|
||||
for cfg in data.get("adb_vector_configs", []):
|
||||
if c.execute("SELECT 1 FROM adb_vector_configs WHERE id=?", (cfg["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
c.execute("INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_dir,wallet_password_enc,table_name,use_mtls,is_active,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(cfg["id"], cfg.get("user_id", u["id"]), cfg.get("config_name",""), cfg.get("dsn",""), cfg.get("username",""),
|
||||
cfg.get("password_enc",""), cfg.get("wallet_dir"), cfg.get("wallet_password_enc"), cfg.get("table_name","CIS_EMBEDDINGS"),
|
||||
cfg.get("use_mtls",1), cfg.get("is_active",1), cfg.get("created_at")))
|
||||
counts["adb_vector_configs"] += 1
|
||||
# ADB vector tables
|
||||
for t in data.get("adb_vector_tables", []):
|
||||
if c.execute("SELECT 1 FROM adb_vector_tables WHERE id=?", (t["id"],)).fetchone():
|
||||
counts["skipped"] += 1; continue
|
||||
c.execute("INSERT INTO adb_vector_tables (id,adb_config_id,table_name,description,is_active,created_at) VALUES (?,?,?,?,?,?)",
|
||||
(t["id"], t.get("adb_config_id",""), t.get("table_name",""), t.get("description",""), t.get("is_active",1), t.get("created_at")))
|
||||
counts["adb_vector_tables"] += 1
|
||||
_audit(u["id"], u["username"], "import_config", "bulk", json.dumps(counts))
|
||||
return counts
|
||||
|
||||
|
||||
# ── CIS Engine Version Management ─────────────────────────────────────────────
|
||||
CIS_REPORTS_PATH = Path("/app/cis_reports.py")
|
||||
CIS_GITHUB_RAW = "https://raw.githubusercontent.com/oci-landing-zones/oci-cis-landingzone-quickstart/main/scripts/cis_reports.py"
|
||||
|
||||
@@ -1824,7 +1824,15 @@ ${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma
|
||||
</div>`:''}</div>`}
|
||||
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()}
|
||||
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
|
||||
return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</div><div id="ocm"></div>
|
||||
return`<div class="cd" style="display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem">
|
||||
<div style="font-size:.78rem;font-weight:600;color:var(--t2)">⚙️ Configuração do Sistema</div>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<button class="btn bp bsm" onclick="cfgExport()">📤 Exportar</button>
|
||||
<button class="btn bs bsm" onclick="document.getElementById('cfgImportFile').click()">📥 Importar</button>
|
||||
<input type="file" id="cfgImportFile" accept=".json" style="display:none" onchange="cfgImport(this)">
|
||||
</div></div>
|
||||
<div id="cfgMsg"></div>
|
||||
<div class="cd"><div class="ct">☁️ Credenciais Registradas</div><div id="ocm"></div>
|
||||
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Detalhes</th><th>Ações</th></tr></thead><tbody>
|
||||
${S.ociCfg.map(c=>`<tr${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td><span class="tag">${c.region}</span></td>
|
||||
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6"><span title="Fingerprint">🔑 ••••••••••••••••••••</span><br><span title="Tenancy OCID">🏢 ${c.tenancy_ocid}</span><br><span title="User OCID">👤 ${c.user_ocid}</span><br><span title="Compartment">📦 ${c.compartment_id||'—'}</span></td>
|
||||
@@ -1862,6 +1870,39 @@ async function sOci(){const tn=document.getElementById('otn').value;
|
||||
const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando credencial...','i');
|
||||
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
|
||||
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociFormRegVal='';S.ociFormRegFilter='';S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
|
||||
async function cfgExport(){
|
||||
try{
|
||||
const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}});
|
||||
if(!resp.ok)throw new Error('Erro ao exportar');
|
||||
const blob=await resp.blob();
|
||||
const url=URL.createObjectURL(blob);
|
||||
const a=document.createElement('a');a.href=url;
|
||||
a.download=resp.headers.get('content-disposition')?.match(/filename="(.+)"/)?.[1]||'config-export.json';
|
||||
a.click();URL.revokeObjectURL(url);
|
||||
sm('cfgMsg','✅ Configuração exportada com sucesso!','s');
|
||||
}catch(e){sm('cfgMsg','❌ '+e.message,'e')}
|
||||
}
|
||||
async function cfgImport(input){
|
||||
const file=input.files[0];if(!file)return;
|
||||
try{
|
||||
const fd=new FormData();fd.append('file',file);
|
||||
const resp=await fetch(API+'/config/import',{method:'POST',headers:S.token?{'Authorization':'Bearer '+S.token}:{},body:fd});
|
||||
if(!resp.ok){const err=await resp.json();throw new Error(err.detail||'Erro ao importar')}
|
||||
const d=await resp.json();
|
||||
const parts=[];
|
||||
if(d.oci_configs)parts.push(d.oci_configs+' OCI configs');
|
||||
if(d.genai_configs)parts.push(d.genai_configs+' GenAI configs');
|
||||
if(d.mcp_servers)parts.push(d.mcp_servers+' MCP servers');
|
||||
if(d.system_prompts)parts.push(d.system_prompts+' prompts');
|
||||
if(d.app_settings)parts.push(d.app_settings+' settings');
|
||||
if(d.adb_vector_configs)parts.push(d.adb_vector_configs+' ADB configs');
|
||||
if(d.skipped)parts.push(d.skipped+' ignorados (já existem)');
|
||||
sm('cfgMsg','✅ Importado: '+parts.join(', '),'s');
|
||||
[S.ociCfg,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/genai/configs'),$api('/mcp/servers')]);
|
||||
R();
|
||||
}catch(e){sm('cfgMsg','❌ '+e.message,'e')}
|
||||
finally{input.value=''}
|
||||
}
|
||||
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?'✅ Conexão OK!':'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}}
|
||||
async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user