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:
nogueiraguh
2026-03-09 15:26:13 -03:00
parent c4a9f8d6d5
commit 188fc82858
2 changed files with 170 additions and 2 deletions

View File

@@ -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"

View File

@@ -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()}