Files
A-Team-Security-Infra-Agent…/backend/routes/oci_explorer.py
nogueiraguh 1135e9d6a9 refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules
Fase 0 complete: extract all business logic, auth, database, and 176
endpoints from monolithic app.py into dedicated modules.

Structure:
- app.py: FastAPI hub (CORS, routers, startup/shutdown)
- models.py: 13 Pydantic request models
- utils.py: shared utilities (embed status, upload validation, process registries)
- config.py: constants, env vars, model catalogs
- auth/: crypto, jwt_auth, oidc, rate_limit
- database/: db(), init_db(), schema DDL
- services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp
- routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer,
  genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine)

Also: README updated with OCIR auth instructions for manual docker run.

86 tests passing.
2026-04-06 15:20:10 -03:00

1015 lines
52 KiB
Python

"""OCI Account Explorer — compartments, regions, resource types, counts cache."""
import threading
import asyncio
from fastapi import APIRouter, Query, Depends, HTTPException
from database import db
from auth.jwt_auth import current_user, require, _audit, _verify_config_access
from auth.crypto import _safe_dec
from services.oci_helpers import _get_oci_config
from config import log
router = APIRouter()
# ── OCI Account Explorer ──────────────────────────────────────────────────────
@router.get("/api/oci/explore/{cid}/compartments")
async def explore_compartments(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
root = _safe_dec(cfg["compartment_id"]) or _safe_dec(cfg["tenancy_ocid"])
comps = identity.list_compartments(root, compartment_id_in_subtree=True).data
return [{"id":cp.id,"name":cp.name,"lifecycle_state":cp.lifecycle_state,"description":cp.description or ""} for cp in comps if cp.lifecycle_state == "ACTIVE"]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/regions")
async def explore_regions(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid FROM oci_configs WHERE id=?", (cid,)).fetchone()
regions = identity.list_region_subscriptions(_safe_dec(cfg["tenancy_ocid"])).data
return [{"name":r.region_name,"key":r.region_key,"status":r.status,"is_home":r.is_home_region} for r in regions]
except Exception as e:
return {"error": str(e)[:500]}
_DEAD_STATES = {'TERMINATED','TERMINATING','DELETED','DELETING','PENDING_DELETION','FAULTY'}
_EXP_RESOURCE_TYPES = [
'instances','boot_volumes','instance_pools',
'vcns','subnets','security_lists','nsgs','route_tables','nat_gateways',
'internet_gateways','service_gateways','public_ips','load_balancers',
'buckets','block_volumes','file_systems','mount_targets',
'databases','db_systems','mysql_db_systems',
'container_instances','oke_clusters',
'functions','api_gateways',
'alarms','log_groups','notification_topics','events_rules',
'vaults','dns_zones','network_firewalls','network_firewall_policies',
'iam_users','iam_groups','iam_policies','iam_dynamic_groups',
]
def _explore_comp(cid):
"""Helper: resolve compartment from config."""
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
return cfg, _safe_dec(cfg["compartment_id"]) or _safe_dec(cfg["tenancy_ocid"])
# ── Explorer Counts Cache ─────────────────────────────────────────────────────
@router.get("/api/oci/explore/{cid}/counts")
async def explore_counts_cached(cid: str, compartment_id: str = Query(...), regions: str = Query(""), u=Depends(current_user)):
"""Return cached resource counts from DB (instant)."""
_verify_config_access("oci", cid, u)
with db() as c:
rows = c.execute(
"SELECT resource_type, count, updated_at FROM explorer_counts_cache WHERE oci_config_id=? AND compartment_id=? AND regions=?",
(cid, compartment_id, regions)).fetchall()
return {r["resource_type"]: {"count": r["count"], "updated_at": r["updated_at"]} for r in rows}
@router.post("/api/oci/explore/{cid}/counts/refresh")
async def explore_counts_refresh(cid: str, req: dict, u=Depends(current_user)):
"""Refresh all resource counts in background thread and save to DB."""
_verify_config_access("oci", cid, u)
compartment_id = req.get("compartment_id")
regions_list = req.get("regions", [])
if not compartment_id:
raise HTTPException(400, "compartment_id required")
regions_key = ",".join(sorted(regions_list)) if regions_list else ""
def _do_refresh():
import httpx, asyncio
_exp_fn_map = {} # will call explore endpoints via internal dispatch
results = {}
for rt in _EXP_RESOURCE_TYPES:
fn_name = f"explore_{rt}"
fn = globals().get(fn_name)
if not fn:
results[rt] = 0
continue
try:
loop = asyncio.new_event_loop()
regs = regions_list if regions_list else [None]
total = 0
for reg in regs:
# IAM endpoints don't take compartment_id/region
if rt.startswith('iam_') and rt != 'iam_policies':
data = loop.run_until_complete(fn(cid, u=u))
elif rt == 'iam_policies':
data = loop.run_until_complete(fn(cid, compartment_id=compartment_id, u=u))
else:
data = loop.run_until_complete(fn(cid, compartment_id=compartment_id, region=reg, u=u))
if isinstance(data, list):
total += len(data)
elif isinstance(data, dict) and not data.get("error"):
total += 0
results[rt] = total
loop.close()
except Exception as e:
log.warning(f"Explorer count refresh error for {rt}: {e}")
results[rt] = 0
# Save to DB
with db() as c:
for rt, cnt in results.items():
c.execute("""INSERT INTO explorer_counts_cache (oci_config_id, compartment_id, resource_type, count, regions, updated_at)
VALUES (?,?,?,?,?,datetime('now'))
ON CONFLICT(oci_config_id, compartment_id, resource_type, regions)
DO UPDATE SET count=excluded.count, updated_at=datetime('now')""",
(cid, compartment_id, rt, cnt, regions_key))
log.info(f"Explorer counts refreshed for config={cid} comp={compartment_id[:20]}... total_types={len(results)}")
threading.Thread(target=_do_refresh, daemon=True).start()
return {"status": "refreshing", "types": len(_EXP_RESOURCE_TYPES)}
@router.get("/api/oci/explore/{cid}/compartment-tree")
async def explore_compartment_tree(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
cfg, default_comp = _explore_comp(cid)
tenancy = _safe_dec(cfg["tenancy_ocid"]) or default_comp
user_comp = _safe_dec(cfg["compartment_id"])
# Use tenancy as root for the tree
tree = [{"id": tenancy, "name": "root (tenancy)", "parent_id": None}]
try:
comps = identity.list_compartments(tenancy, compartment_id_in_subtree=True).data
seen = {tenancy}
for cp in comps:
if cp.lifecycle_state == "ACTIVE":
tree.append({"id":cp.id, "name":cp.name, "parent_id":cp.compartment_id,
"lifecycle_state":cp.lifecycle_state})
seen.add(cp.id)
# If user's configured compartment is not in the tree, add it explicitly
if user_comp and user_comp != tenancy and user_comp not in seen:
try:
cp = identity.get_compartment(user_comp).data
tree.append({"id":cp.id, "name":cp.name, "parent_id":cp.compartment_id,
"lifecycle_state":cp.lifecycle_state})
except Exception as e:
log.warning(f"Failed to get compartment {user_comp}: {e}")
tree.append({"id":user_comp, "name":"Meu Compartment", "parent_id":tenancy,
"lifecycle_state":"ACTIVE"})
except Exception as e:
log.warning(f"Failed to list compartments from tenancy: {e}")
# If listing from tenancy fails (no permission), list from user's compartment
if user_comp and user_comp != tenancy:
try:
cp = identity.get_compartment(user_comp).data
tree = [{"id":cp.id, "name":cp.name, "parent_id": None, "lifecycle_state":cp.lifecycle_state}]
sub = identity.list_compartments(user_comp, compartment_id_in_subtree=True).data
tree += [{"id":s.id, "name":s.name, "parent_id":s.compartment_id,
"lifecycle_state":s.lifecycle_state} for s in sub if s.lifecycle_state == "ACTIVE"]
except Exception as e:
log.warning(f"Failed to list sub-compartments for {user_comp}: {e}")
tree = [{"id":user_comp, "name":"Meu Compartment", "parent_id": None, "lifecycle_state":"ACTIVE"}]
return tree
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/vcns")
async def explore_vcns(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
vcns = vn.list_vcns(comp).data
return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state,
"dns_label":v.dns_label,"vcn_domain_name":v.vcn_domain_name,
"default_route_table_id":v.default_route_table_id,"default_security_list_id":v.default_security_list_id,
"time_created":str(v.time_created)} for v in vcns if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/instances")
async def explore_instances(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
compute = oci.core.ComputeClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
insts = compute.list_instances(comp).data
return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts if i.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/databases")
async def explore_databases(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
db_client = oci.database.DatabaseClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
adbs = db_client.list_autonomous_databases(comp).data
return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state,
"cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs,
"is_free_tier":a.is_free_tier,"whitelisted_ips":a.whitelisted_ips or []} for a in adbs if a.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/buckets")
async def explore_buckets(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
os_client = oci.object_storage.ObjectStorageClient(config)
namespace = os_client.get_namespace().data
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
buckets = os_client.list_buckets(namespace, comp, fields=["approximateSize","approximateCount"]).data
return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created),
"approximate_size":b.approximate_size,"approximate_count":b.approximate_count,
"public_access_type":b.public_access_type or "NoPublicAccess",
"storage_tier":b.storage_tier,"versioning":b.versioning} for b in buckets]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/subnets")
async def explore_subnets(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
subs = vn.list_subnets(comp).data
return [{"id":s.id,"display_name":s.display_name,"cidr_block":s.cidr_block,"vcn_id":s.vcn_id,
"lifecycle_state":s.lifecycle_state,"prohibit_public_ip_on_vnic":s.prohibit_public_ip_on_vnic,
"availability_domain":s.availability_domain or "Regional","dns_label":s.dns_label,
"route_table_id":s.route_table_id,"security_list_ids":s.security_list_ids,
"subnet_domain_name":s.subnet_domain_name,"time_created":str(s.time_created)} for s in subs if s.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/security_lists")
async def explore_security_lists(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
sls = vn.list_security_lists(comp).data
def _fmt_rule(r, direction, idx):
proto_map = {"6":"TCP","17":"UDP","1":"ICMP","all":"ALL"}
proto = proto_map.get(r.protocol, r.protocol)
src_dst = r.source if direction == "ingress" else r.destination
ports = ""
if r.tcp_options:
dp = r.tcp_options.destination_port_range
if dp: ports = f"{dp.min}-{dp.max}" if dp.min != dp.max else str(dp.min)
elif r.udp_options:
dp = r.udp_options.destination_port_range
if dp: ports = f"{dp.min}-{dp.max}" if dp.min != dp.max else str(dp.min)
return {"direction":direction,"protocol":proto,"source_dest":src_dst or "","ports":ports,
"stateless":r.is_stateless,"description":getattr(r,'description','') or "","rule_index":idx}
result = []
for s in sls:
if s.lifecycle_state in _DEAD_STATES: continue
ingress = [_fmt_rule(r,"ingress",i) for i,r in enumerate(s.ingress_security_rules or [])]
egress = [_fmt_rule(r,"egress",i) for i,r in enumerate(s.egress_security_rules or [])]
result.append({"id":s.id,"display_name":s.display_name,"vcn_id":s.vcn_id,"lifecycle_state":s.lifecycle_state,
"ingress_count":len(ingress),"egress_count":len(egress),"rules":ingress+egress})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.post("/api/oci/explore/{cid}/security_lists/{sl_id}/rules")
async def add_security_list_rule(cid: str, sl_id: str, req: dict, u=Depends(require("admin", "user"))):
"""Add a rule to a Security List (appends to existing rules)."""
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
region = req.get("region")
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
sl = vn.get_security_list(sl_id).data
direction = req.get("direction", "ingress").lower()
protocol = str(req.get("protocol", "6"))
source_dest = req.get("source_dest", "0.0.0.0/0")
port_min = req.get("port_min")
port_max = req.get("port_max")
description = req.get("description", "").strip() or None
is_stateless = req.get("is_stateless", False)
tcp_opts = None
udp_opts = None
if protocol == "6" and port_min is not None:
tcp_opts = oci.core.models.TcpOptions(
destination_port_range=oci.core.models.PortRange(min=int(port_min), max=int(port_max or port_min)))
elif protocol == "17" and port_min is not None:
udp_opts = oci.core.models.UdpOptions(
destination_port_range=oci.core.models.PortRange(min=int(port_min), max=int(port_max or port_min)))
ingress_rules = list(sl.ingress_security_rules or [])
egress_rules = list(sl.egress_security_rules or [])
if direction == "ingress":
ingress_rules.append(oci.core.models.IngressSecurityRule(
protocol=protocol, source=source_dest, source_type="CIDR_BLOCK",
is_stateless=is_stateless, description=description,
tcp_options=tcp_opts, udp_options=udp_opts))
else:
egress_rules.append(oci.core.models.EgressSecurityRule(
protocol=protocol, destination=source_dest, destination_type="CIDR_BLOCK",
is_stateless=is_stateless, description=description,
tcp_options=tcp_opts, udp_options=udp_opts))
update = oci.core.models.UpdateSecurityListDetails(
ingress_security_rules=ingress_rules, egress_security_rules=egress_rules)
vn.update_security_list(sl_id, update)
_audit(u["id"], u["username"], "seclist_add_rule", sl_id,
f"{direction} {protocol} {source_dest} port={port_min or 'ALL'}")
return {"ok": True, "message": "Regra adicionada com sucesso"}
except Exception as e:
raise HTTPException(400, str(e)[:500])
@router.delete("/api/oci/explore/{cid}/security_lists/{sl_id}/rules")
async def remove_security_list_rule(cid: str, sl_id: str, req: dict, u=Depends(require("admin", "user"))):
"""Remove a rule from a Security List by index."""
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
region = req.get("region")
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
sl = vn.get_security_list(sl_id).data
direction = req.get("direction", "ingress").lower()
rule_index = req.get("rule_index")
if rule_index is None: raise HTTPException(400, "rule_index obrigatório")
ingress_rules = list(sl.ingress_security_rules or [])
egress_rules = list(sl.egress_security_rules or [])
if direction == "ingress":
if 0 <= rule_index < len(ingress_rules):
removed = ingress_rules.pop(rule_index)
desc = f"ingress idx={rule_index} src={removed.source}"
else:
raise HTTPException(400, "Índice inválido")
else:
if 0 <= rule_index < len(egress_rules):
removed = egress_rules.pop(rule_index)
desc = f"egress idx={rule_index} dst={removed.destination}"
else:
raise HTTPException(400, "Índice inválido")
update = oci.core.models.UpdateSecurityListDetails(
ingress_security_rules=ingress_rules, egress_security_rules=egress_rules)
vn.update_security_list(sl_id, update)
_audit(u["id"], u["username"], "seclist_remove_rule", sl_id, desc)
return {"ok": True, "message": "Regra removida com sucesso"}
except Exception as e:
raise HTTPException(400, str(e)[:500])
@router.get("/api/oci/explore/{cid}/block_volumes")
async def explore_block_volumes(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
bs = oci.core.BlockstorageClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
vols = bs.list_volumes(compartment_id=comp).data
return [{"id":v.id,"display_name":v.display_name,"size_in_gbs":v.size_in_gbs,
"lifecycle_state":v.lifecycle_state,"vpus_per_gb":v.vpus_per_gb} for v in vols if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/load_balancers")
async def explore_load_balancers(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
lb = oci.load_balancer.LoadBalancerClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
lbs = lb.list_load_balancers(comp).data
result = []
for l in lbs:
if l.lifecycle_state in _DEAD_STATES: continue
listeners = []
for name, lis in (l.listeners or {}).items():
listeners.append({"name": name, "port": lis.port, "protocol": lis.protocol,
"backend_set": lis.default_backend_set_name})
backend_sets = []
for name, bs in (l.backend_sets or {}).items():
backends = [{"ip": b.ip_address, "port": b.port, "weight": b.weight} for b in (bs.backends or [])]
backend_sets.append({"name": name, "backends": backends, "policy": bs.policy})
result.append({"id":l.id,"display_name":l.display_name,"lifecycle_state":l.lifecycle_state,
"shape_name":l.shape_name,"ip_addresses":[ip.ip_address for ip in (l.ip_addresses or [])],
"is_private":l.is_private,"listeners":listeners,"backend_sets":backend_sets})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/functions")
async def explore_functions(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
fn = oci.functions.FunctionsManagementClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
apps = fn.list_applications(comp).data
result = []
for a in apps:
if a.lifecycle_state in _DEAD_STATES: continue
fns = fn.list_functions(a.id).data
for f in fns:
if f.lifecycle_state in _DEAD_STATES: continue
result.append({"id":f.id,"display_name":f.display_name,"application":a.display_name,
"lifecycle_state":f.lifecycle_state,"memory_in_mbs":f.memory_in_mbs,
"timeout_in_seconds":f.timeout_in_seconds,"image":f.image})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/container_instances")
async def explore_container_instances(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
ci = oci.container_instances.ContainerInstanceClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
cis = ci.list_container_instances(comp).data
return [{"id":c.id,"display_name":c.display_name,"lifecycle_state":c.lifecycle_state,
"container_count":c.container_count,"shape":c.shape,
"time_created":str(c.time_created)} for c in cis if c.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/nsgs")
async def explore_nsgs(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
nsgs = vn.list_network_security_groups(compartment_id=comp).data
result = []
proto_map = {"1": "ICMP", "6": "TCP", "17": "UDP", "all": "ALL"}
for n in nsgs:
if n.lifecycle_state in _DEAD_STATES: continue
rules_data = oci.pagination.list_call_get_all_results(
vn.list_network_security_group_security_rules, n.id).data
rules = []
for r in rules_data:
ports = ""
if r.tcp_options and r.tcp_options.destination_port_range:
pr = r.tcp_options.destination_port_range
ports = str(pr.min) if pr.min == pr.max else f"{pr.min}-{pr.max}"
elif r.udp_options and r.udp_options.destination_port_range:
pr = r.udp_options.destination_port_range
ports = str(pr.min) if pr.min == pr.max else f"{pr.min}-{pr.max}"
src_dst = ""
src_dst_type = ""
if r.direction == "INGRESS":
src_dst = r.source or ""
src_dst_type = r.source_type or ""
else:
src_dst = r.destination or ""
src_dst_type = r.destination_type or ""
rules.append({
"id": r.id, "direction": r.direction,
"protocol": proto_map.get(r.protocol, r.protocol),
"protocol_num": r.protocol,
"source_dest": src_dst, "source_dest_type": src_dst_type,
"ports": ports, "is_stateless": r.is_stateless or False,
"description": r.description or ""
})
result.append({"id": n.id, "display_name": n.display_name, "vcn_id": n.vcn_id,
"lifecycle_state": n.lifecycle_state, "time_created": str(n.time_created),
"rule_count": len(rules), "rules": rules})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.post("/api/oci/explore/{cid}/nsgs/{nsg_id}/rules")
async def add_nsg_rule(cid: str, nsg_id: str, req: dict, u=Depends(require("admin", "user"))):
"""Add a security rule to an NSG."""
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
region = req.get("region")
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
direction = req.get("direction", "INGRESS").upper()
protocol = str(req.get("protocol", "6"))
source_dest = req.get("source_dest", "0.0.0.0/0")
source_dest_type = req.get("source_dest_type", "CIDR_BLOCK")
port_min = req.get("port_min")
port_max = req.get("port_max")
description = req.get("description", "")
is_stateless = req.get("is_stateless", False)
rule = {"direction": direction, "protocol": protocol, "is_stateless": is_stateless, "description": description}
if direction == "INGRESS":
rule["source"] = source_dest
rule["source_type"] = source_dest_type
else:
rule["destination"] = source_dest
rule["destination_type"] = source_dest_type
if protocol == "6" and port_min is not None:
rule["tcp_options"] = oci.core.models.TcpOptions(
destination_port_range=oci.core.models.PortRange(min=int(port_min), max=int(port_max or port_min)))
elif protocol == "17" and port_min is not None:
rule["udp_options"] = oci.core.models.UdpOptions(
destination_port_range=oci.core.models.PortRange(min=int(port_min), max=int(port_max or port_min)))
details = oci.core.models.AddNetworkSecurityGroupSecurityRulesDetails(
security_rules=[oci.core.models.AddSecurityRuleDetails(**rule)])
resp = vn.add_network_security_group_security_rules(nsg_id, details)
_audit(u["id"], u["username"], "nsg_add_rule", nsg_id, f"{direction} {protocol} {source_dest} {port_min or 'ALL'}")
added = [{"id": r.id} for r in (resp.data.security_rules or [])]
return {"ok": True, "message": "Regra adicionada com sucesso", "rules_added": added}
except Exception as e:
raise HTTPException(400, str(e)[:500])
@router.delete("/api/oci/explore/{cid}/nsgs/{nsg_id}/rules/{rule_id}")
async def remove_nsg_rule(cid: str, nsg_id: str, rule_id: str, region: str = Query(None), u=Depends(require("admin", "user"))):
"""Remove a security rule from an NSG."""
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
details = oci.core.models.RemoveNetworkSecurityGroupSecurityRulesDetails(security_rule_ids=[rule_id])
vn.remove_network_security_group_security_rules(nsg_id, details)
_audit(u["id"], u["username"], "nsg_remove_rule", nsg_id, f"rule_id={rule_id}")
return {"ok": True, "message": "Regra removida com sucesso"}
except Exception as e:
raise HTTPException(400, str(e)[:500])
@router.get("/api/oci/explore/{cid}/route_tables")
async def explore_route_tables(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
rts = vn.list_route_tables(comp).data
result = []
for rt in rts:
if rt.lifecycle_state in _DEAD_STATES: continue
routes = []
for r in (rt.route_rules or []):
target_type = r.network_entity_id.split(".")[1] if r.network_entity_id and "." in r.network_entity_id else "unknown"
routes.append({"destination":r.destination or "","destination_type":r.destination_type or "",
"target_id":r.network_entity_id or "","target_type":target_type,
"description":getattr(r,'description','') or ""})
result.append({"id":rt.id,"display_name":rt.display_name,"vcn_id":rt.vcn_id,"lifecycle_state":rt.lifecycle_state,
"route_count":len(routes),"routes":routes})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/nat_gateways")
async def explore_nat_gateways(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
ngs = vn.list_nat_gateways(comp).data
return [{"id":n.id,"display_name":n.display_name,"vcn_id":n.vcn_id,"lifecycle_state":n.lifecycle_state,
"nat_ip":n.nat_ip,"block_traffic":n.block_traffic} for n in ngs if n.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/internet_gateways")
async def explore_internet_gateways(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
igs = vn.list_internet_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"vcn_id":g.vcn_id,"lifecycle_state":g.lifecycle_state,
"is_enabled":g.is_enabled} for g in igs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/service_gateways")
async def explore_service_gateways(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
sgs = vn.list_service_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"vcn_id":g.vcn_id,"lifecycle_state":g.lifecycle_state,
"services":[s.service_name for s in (g.services or [])]} for g in sgs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/public_ips")
async def explore_public_ips(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
vn = oci.core.VirtualNetworkClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
ips = vn.list_public_ips(scope="REGION", compartment_id=comp).data
return [{"id":ip.id,"display_name":ip.display_name or "\u2014","ip_address":ip.ip_address,
"lifecycle_state":ip.lifecycle_state,"lifetime":ip.lifetime,
"assigned_entity_type":ip.assigned_entity_type} for ip in ips if ip.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/boot_volumes")
async def explore_boot_volumes(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
identity = oci.identity.IdentityClient(config)
bs = oci.core.BlockstorageClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
ads = identity.list_availability_domains(comp).data
result = []
for ad in ads:
bvs = bs.list_boot_volumes(availability_domain=ad.name, compartment_id=comp).data
for v in bvs:
if v.lifecycle_state in _DEAD_STATES: continue
result.append({"id":v.id,"display_name":v.display_name,"size_in_gbs":v.size_in_gbs,
"lifecycle_state":v.lifecycle_state,"availability_domain":ad.name,
"vpus_per_gb":v.vpus_per_gb})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/instance_pools")
async def explore_instance_pools(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
cm = oci.core.ComputeManagementClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
pools = cm.list_instance_pools(comp).data
return [{"id":p.id,"display_name":p.display_name,"lifecycle_state":p.lifecycle_state,
"size":p.size,"time_created":str(p.time_created)} for p in pools if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/oke_clusters")
async def explore_oke_clusters(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
ce = oci.container_engine.ContainerEngineClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
clusters = ce.list_clusters(comp).data
return [{"id":c.id,"name":c.name,"lifecycle_state":c.lifecycle_state,
"kubernetes_version":c.kubernetes_version,"vcn_id":c.vcn_id,
"endpoint_config":c.endpoint_config.is_public_ip_enabled if c.endpoint_config else None} for c in clusters if c.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/file_systems")
async def explore_file_systems(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
identity = oci.identity.IdentityClient(config)
fss = oci.file_storage.FileStorageClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
ads = identity.list_availability_domains(comp).data
result = []
for ad in ads:
fs_list = fss.list_file_systems(comp, ad.name).data
for f in fs_list:
if f.lifecycle_state in _DEAD_STATES: continue
result.append({"id":f.id,"display_name":f.display_name,"lifecycle_state":f.lifecycle_state,
"availability_domain":ad.name,"metered_bytes":f.metered_bytes,
"time_created":str(f.time_created)})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/mount_targets")
async def explore_mount_targets(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
identity = oci.identity.IdentityClient(config)
fss = oci.file_storage.FileStorageClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
ads = identity.list_availability_domains(comp).data
result = []
for ad in ads:
mts = fss.list_mount_targets(comp, ad.name).data
for m in mts:
if m.lifecycle_state in _DEAD_STATES: continue
result.append({"id":m.id,"display_name":m.display_name,"lifecycle_state":m.lifecycle_state,
"availability_domain":ad.name,"subnet_id":m.subnet_id,
"private_ip_ids":m.private_ip_ids})
return result
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/db_systems")
async def explore_db_systems(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
db_client = oci.database.DatabaseClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
dbs = db_client.list_db_systems(comp).data
return [{"id":d.id,"display_name":d.display_name,"lifecycle_state":d.lifecycle_state,
"shape":d.shape,"database_edition":d.database_edition,
"cpu_core_count":d.cpu_core_count,"node_count":d.node_count} for d in dbs if d.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/mysql_db_systems")
async def explore_mysql_db_systems(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
mysql = oci.mysql.DbSystemClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
dbs = mysql.list_db_systems(comp).data
return [{"id":d.id,"display_name":d.display_name,"lifecycle_state":d.lifecycle_state,
"shape_name":d.shape_name,"mysql_version":d.mysql_version,
"is_highly_available":d.is_highly_available} for d in dbs if d.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/alarms")
async def explore_alarms(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
mon = oci.monitoring.MonitoringClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
alarms = mon.list_alarms(comp).data
return [{"id":a.id,"display_name":a.display_name,"lifecycle_state":a.lifecycle_state,
"severity":a.severity,"namespace":a.namespace,"is_enabled":a.is_enabled} for a in alarms if a.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/log_groups")
async def explore_log_groups(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
logging_client = oci.logging.LoggingManagementClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
lgs = logging_client.list_log_groups(comp).data
return [{"id":g.id,"display_name":g.display_name,"time_created":str(g.time_created),
"description":g.description or ""} for g in lgs]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/iam_users")
async def explore_iam_users(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
cfg, _ = _explore_comp(cid)
tenancy = _safe_dec(cfg["tenancy_ocid"])
users = identity.list_users(tenancy).data
return [{"id":u2.id,"name":u2.name,"email":u2.email or "","lifecycle_state":u2.lifecycle_state,
"is_mfa_activated":u2.is_mfa_activated,"time_created":str(u2.time_created),
"last_successful_login_time":str(u2.last_successful_login_time) if u2.last_successful_login_time else None} for u2 in users if u2.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/iam_groups")
async def explore_iam_groups(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
cfg, _ = _explore_comp(cid)
tenancy = _safe_dec(cfg["tenancy_ocid"])
groups = identity.list_groups(tenancy).data
return [{"id":g.id,"name":g.name,"description":g.description or "","lifecycle_state":g.lifecycle_state,
"time_created":str(g.time_created)} for g in groups if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/iam_policies")
async def explore_iam_policies(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
cfg, default_comp = _explore_comp(cid)
comp = compartment_id or _safe_dec(cfg["tenancy_ocid"]) or default_comp
policies = identity.list_policies(comp).data
return [{"id":p.id,"name":p.name,"description":p.description or "","lifecycle_state":p.lifecycle_state,
"statements":p.statements,"time_created":str(p.time_created)} for p in policies if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/iam_dynamic_groups")
async def explore_iam_dynamic_groups(cid: str, u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
cfg, _ = _explore_comp(cid)
tenancy = _safe_dec(cfg["tenancy_ocid"])
dgs = identity.list_dynamic_groups(tenancy).data
return [{"id":g.id,"name":g.name,"description":g.description or "","lifecycle_state":g.lifecycle_state,
"matching_rule":g.matching_rule,"time_created":str(g.time_created)} for g in dgs if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/vaults")
async def explore_vaults(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
kms = oci.key_management.KmsVaultClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
vaults = kms.list_vaults(comp).data
return [{"id":v.id,"display_name":v.display_name,"lifecycle_state":v.lifecycle_state,
"vault_type":v.vault_type,"crypto_endpoint":v.crypto_endpoint,
"time_created":str(v.time_created)} for v in vaults if v.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/dns_zones")
async def explore_dns_zones(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
dns = oci.dns.DnsClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
zones = dns.list_zones(comp).data
return [{"id":z.id,"name":z.name,"lifecycle_state":z.lifecycle_state,
"zone_type":z.zone_type,"serial":z.serial,
"time_created":str(z.time_created)} for z in zones if z.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/api_gateways")
async def explore_api_gateways(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
apigw = oci.apigateway.GatewayClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
gws = apigw.list_gateways(comp).data
return [{"id":g.id,"display_name":g.display_name,"lifecycle_state":g.lifecycle_state,
"endpoint_type":g.endpoint_type,"hostname":g.hostname,
"subnet_id":g.subnet_id,"time_created":str(g.time_created)} for g in gws if g.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/notification_topics")
async def explore_notification_topics(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
ons = oci.ons.NotificationControlPlaneClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
topics = ons.list_topics(comp).data
return [{"id":t.topic_id,"name":t.name,"lifecycle_state":t.lifecycle_state,
"description":t.description or "","api_endpoint":t.api_endpoint,
"time_created":str(t.time_created)} for t in topics if t.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/events_rules")
async def explore_events_rules(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
events = oci.events.EventsClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
rules = events.list_rules(comp).data
return [{"id":r.id,"display_name":r.display_name,"lifecycle_state":r.lifecycle_state,
"condition":r.condition,"is_enabled":r.is_enabled,
"description":r.description or "","time_created":str(r.time_created)} for r in rules if r.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/network_firewalls")
async def explore_network_firewalls(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
fw = oci.network_firewall.NetworkFirewallClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
resp = fw.list_network_firewalls(compartment_id=comp).data
firewalls = resp.items if hasattr(resp, 'items') else (resp if isinstance(resp, list) else [])
return [{"id":f.id,"display_name":f.display_name,"lifecycle_state":f.lifecycle_state,
"subnet_id":f.subnet_id,"network_firewall_policy_id":f.network_firewall_policy_id,
"time_created":str(f.time_created)} for f in firewalls if f.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}
@router.get("/api/oci/explore/{cid}/network_firewall_policies")
async def explore_network_firewall_policies(cid: str, compartment_id: str = Query(None), region: str = Query(None), u=Depends(current_user)):
_verify_config_access("oci", cid, u)
try:
import oci
config = _get_oci_config(cid)
if region: config["region"] = region
fw = oci.network_firewall.NetworkFirewallClient(config)
_, default_comp = _explore_comp(cid)
comp = compartment_id or default_comp
resp = fw.list_network_firewall_policies(compartment_id=comp).data
policies = resp.items if hasattr(resp, 'items') else (resp if isinstance(resp, list) else [])
return [{"id":p.id,"display_name":p.display_name,"lifecycle_state":p.lifecycle_state,
"time_created":str(p.time_created)} for p in policies if p.lifecycle_state not in _DEAD_STATES]
except Exception as e:
return {"error": str(e)[:500]}