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 Query, Body, BackgroundTasks
) )
from fastapi.middleware.cors import CORSMiddleware 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 fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel from pydantic import BaseModel
import jwt as pyjwt 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,)) c.execute("DELETE FROM system_prompts WHERE id=?", (pid,))
return {"ok": True} 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 Engine Version Management ─────────────────────────────────────────────
CIS_REPORTS_PATH = Path("/app/cis_reports.py") 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" 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>`} </div>`:''}</div>`}
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()} 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; 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> <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> ${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> <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'); 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'; 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'}} 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 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()} async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}