feat: Explorer KPI stats, enhanced views, start/stop expansion, Prompt Generator docs injection
- Explorer: KPI stats bar with cached counts (SQLite), background refresh, enhanced views for VCNs/Subnets/LBs/Buckets, start/stop for DB Systems, MySQL, Container Instances, dead state filtering across all 36 endpoints - Prompt Generator: same knowledge pipeline as Terraform Agent — resource type detection + official docs from GitHub + compact resource reference - Fix: NetworkFirewallPolicySummaryCollection not iterable (.items accessor)
This commit is contained in:
282
backend/app.py
282
backend/app.py
@@ -336,6 +336,16 @@ def init_db():
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS explorer_counts_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oci_config_id TEXT NOT NULL,
|
||||
compartment_id TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
regions TEXT DEFAULT '',
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(oci_config_id, compartment_id, resource_type, regions)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tf_valid_types (
|
||||
name TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'resource',
|
||||
@@ -825,12 +835,89 @@ async def explore_regions(cid: str, u=Depends(current_user)):
|
||||
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 ─────────────────────────────────────────────────────
|
||||
@app.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)."""
|
||||
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}
|
||||
|
||||
@app.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."""
|
||||
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 ""
|
||||
import threading
|
||||
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)}
|
||||
|
||||
@app.get("/api/oci/explore/{cid}/compartment-tree")
|
||||
async def explore_compartment_tree(cid: str, u=Depends(current_user)):
|
||||
try:
|
||||
@@ -884,7 +971,10 @@ async def explore_vcns(cid: str, compartment_id: str = Query(None), region: str
|
||||
_, 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} for v in vcns]
|
||||
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]}
|
||||
|
||||
@@ -898,7 +988,7 @@ async def explore_instances(cid: str, compartment_id: str = Query(None), region:
|
||||
_, 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]
|
||||
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]}
|
||||
|
||||
@@ -914,7 +1004,7 @@ async def explore_databases(cid: str, compartment_id: str = Query(None), region:
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -928,8 +1018,11 @@ async def explore_buckets(cid: str, compartment_id: str = Query(None), region: s
|
||||
namespace = os_client.get_namespace().data
|
||||
_, default_comp = _explore_comp(cid)
|
||||
comp = compartment_id or default_comp
|
||||
buckets = os_client.list_buckets(namespace, comp).data
|
||||
return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets]
|
||||
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]}
|
||||
|
||||
@@ -944,7 +1037,10 @@ async def explore_subnets(cid: str, compartment_id: str = Query(None), region: s
|
||||
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} for s in subs]
|
||||
"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]}
|
||||
|
||||
@@ -973,6 +1069,7 @@ async def explore_security_lists(cid: str, compartment_id: str = Query(None), re
|
||||
"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,
|
||||
@@ -1073,7 +1170,7 @@ async def explore_block_volumes(cid: str, compartment_id: str = Query(None), reg
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1087,8 +1184,21 @@ async def explore_load_balancers(cid: str, compartment_id: str = Query(None), re
|
||||
_, default_comp = _explore_comp(cid)
|
||||
comp = compartment_id or default_comp
|
||||
lbs = lb.list_load_balancers(comp).data
|
||||
return [{"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 [])]} for l in lbs]
|
||||
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]}
|
||||
|
||||
@@ -1104,8 +1214,10 @@ async def explore_functions(cid: str, compartment_id: str = Query(None), region:
|
||||
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})
|
||||
@@ -1125,7 +1237,7 @@ async def explore_container_instances(cid: str, compartment_id: str = Query(None
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1142,6 +1254,7 @@ async def explore_nsgs(cid: str, compartment_id: str = Query(None), region: str
|
||||
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 = []
|
||||
@@ -1242,6 +1355,7 @@ async def explore_route_tables(cid: str, compartment_id: str = Query(None), regi
|
||||
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"
|
||||
@@ -1265,7 +1379,7 @@ async def explore_nat_gateways(cid: str, compartment_id: str = Query(None), regi
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1280,7 +1394,7 @@ async def explore_internet_gateways(cid: str, compartment_id: str = Query(None),
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1295,7 +1409,7 @@ async def explore_service_gateways(cid: str, compartment_id: str = Query(None),
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1311,7 +1425,7 @@ async def explore_public_ips(cid: str, compartment_id: str = Query(None), region
|
||||
ips = vn.list_public_ips(scope="REGION", compartment_id=comp).data
|
||||
return [{"id":ip.id,"display_name":ip.display_name or "—","ip_address":ip.ip_address,
|
||||
"lifecycle_state":ip.lifecycle_state,"lifetime":ip.lifetime,
|
||||
"assigned_entity_type":ip.assigned_entity_type} for ip in ips]
|
||||
"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]}
|
||||
|
||||
@@ -1330,6 +1444,7 @@ async def explore_boot_volumes(cid: str, compartment_id: str = Query(None), regi
|
||||
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})
|
||||
@@ -1348,7 +1463,7 @@ async def explore_instance_pools(cid: str, compartment_id: str = Query(None), re
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1364,7 +1479,7 @@ async def explore_oke_clusters(cid: str, compartment_id: str = Query(None), regi
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1383,6 +1498,7 @@ async def explore_file_systems(cid: str, compartment_id: str = Query(None), regi
|
||||
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)})
|
||||
@@ -1405,6 +1521,7 @@ async def explore_mount_targets(cid: str, compartment_id: str = Query(None), reg
|
||||
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})
|
||||
@@ -1424,7 +1541,7 @@ async def explore_db_systems(cid: str, compartment_id: str = Query(None), region
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1440,7 +1557,7 @@ async def explore_mysql_db_systems(cid: str, compartment_id: str = Query(None),
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1455,7 +1572,7 @@ async def explore_alarms(cid: str, compartment_id: str = Query(None), region: st
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1485,7 +1602,7 @@ async def explore_iam_users(cid: str, u=Depends(current_user)):
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1499,7 +1616,7 @@ async def explore_iam_groups(cid: str, u=Depends(current_user)):
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1513,7 +1630,7 @@ async def explore_iam_policies(cid: str, compartment_id: str = Query(None), u=De
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1527,7 +1644,7 @@ async def explore_iam_dynamic_groups(cid: str, u=Depends(current_user)):
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1543,7 +1660,7 @@ async def explore_vaults(cid: str, compartment_id: str = Query(None), region: st
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1559,7 +1676,7 @@ async def explore_dns_zones(cid: str, compartment_id: str = Query(None), region:
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1575,7 +1692,7 @@ async def explore_api_gateways(cid: str, compartment_id: str = Query(None), regi
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1591,7 +1708,7 @@ async def explore_notification_topics(cid: str, compartment_id: str = Query(None
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1607,7 +1724,7 @@ async def explore_events_rules(cid: str, compartment_id: str = Query(None), regi
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1620,10 +1737,11 @@ async def explore_network_firewalls(cid: str, compartment_id: str = Query(None),
|
||||
fw = oci.network_firewall.NetworkFirewallClient(config)
|
||||
_, default_comp = _explore_comp(cid)
|
||||
comp = compartment_id or default_comp
|
||||
firewalls = fw.list_network_firewalls(compartment_id=comp).data
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -1636,9 +1754,10 @@ async def explore_network_firewall_policies(cid: str, compartment_id: str = Quer
|
||||
fw = oci.network_firewall.NetworkFirewallClient(config)
|
||||
_, default_comp = _explore_comp(cid)
|
||||
comp = compartment_id or default_comp
|
||||
policies = fw.list_network_firewall_policies(compartment_id=comp).data
|
||||
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]
|
||||
"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]}
|
||||
|
||||
@@ -4574,6 +4693,79 @@ async def oci_adb_update_network(adb_id: str, req: dict, u=Depends(current_user)
|
||||
raise HTTPException(500, str(e)[:500])
|
||||
|
||||
|
||||
@app.post("/api/oci/db-systems/{db_system_id}/action")
|
||||
async def oci_db_system_action(db_system_id: str, req: dict, u=Depends(current_user)):
|
||||
action = req.get("action", "").upper()
|
||||
oci_config_id = req.get("oci_config_id", "")
|
||||
region = req.get("region")
|
||||
if action not in ("START", "STOP"):
|
||||
raise HTTPException(400, "Ação inválida. Use START ou STOP.")
|
||||
if not oci_config_id:
|
||||
raise HTTPException(400, "oci_config_id obrigatório")
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(oci_config_id)
|
||||
if region: config["region"] = region
|
||||
db_client = oci.database.DatabaseClient(config)
|
||||
if action == "START":
|
||||
db_client.start_db_system(db_system_id)
|
||||
else:
|
||||
db_client.stop_db_system(db_system_id)
|
||||
_audit(u["id"], u["username"], f"db_system_{action.lower()}", db_system_id)
|
||||
return {"ok": True, "action": action, "db_system_id": db_system_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e)[:500])
|
||||
|
||||
|
||||
@app.post("/api/oci/mysql-db-systems/{db_system_id}/action")
|
||||
async def oci_mysql_action(db_system_id: str, req: dict, u=Depends(current_user)):
|
||||
action = req.get("action", "").lower()
|
||||
oci_config_id = req.get("oci_config_id", "")
|
||||
region = req.get("region")
|
||||
if action not in ("start", "stop"):
|
||||
raise HTTPException(400, "Ação inválida. Use start ou stop.")
|
||||
if not oci_config_id:
|
||||
raise HTTPException(400, "oci_config_id obrigatório")
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(oci_config_id)
|
||||
if region: config["region"] = region
|
||||
mysql = oci.mysql.DbSystemClient(config)
|
||||
if action == "start":
|
||||
mysql.start_db_system(db_system_id)
|
||||
else:
|
||||
shutdown_type = req.get("shutdown_type", "SLOW")
|
||||
mysql.stop_db_system(db_system_id, oci.mysql.models.StopDbSystemDetails(shutdown_type=shutdown_type))
|
||||
_audit(u["id"], u["username"], f"mysql_{action}", db_system_id)
|
||||
return {"ok": True, "action": action, "db_system_id": db_system_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e)[:500])
|
||||
|
||||
|
||||
@app.post("/api/oci/container-instances/{ci_id}/action")
|
||||
async def oci_container_instance_action(ci_id: str, req: dict, u=Depends(current_user)):
|
||||
action = req.get("action", "").lower()
|
||||
oci_config_id = req.get("oci_config_id", "")
|
||||
region = req.get("region")
|
||||
if action not in ("start", "stop"):
|
||||
raise HTTPException(400, "Ação inválida. Use start ou stop.")
|
||||
if not oci_config_id:
|
||||
raise HTTPException(400, "oci_config_id obrigatório")
|
||||
try:
|
||||
import oci
|
||||
config = _get_oci_config(oci_config_id)
|
||||
if region: config["region"] = region
|
||||
ci = oci.container_instances.ContainerInstanceClient(config)
|
||||
if action == "start":
|
||||
ci.start_container_instance(ci_id)
|
||||
else:
|
||||
ci.stop_container_instance(ci_id)
|
||||
_audit(u["id"], u["username"], f"container_instance_{action}", ci_id)
|
||||
return {"ok": True, "action": action, "container_instance_id": ci_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e)[:500])
|
||||
|
||||
|
||||
@app.get("/api/oci/my-ip")
|
||||
async def get_my_ip(request: Request):
|
||||
"""Return the caller's public IP address."""
|
||||
@@ -4684,11 +4876,33 @@ async def tf_generate_prompt(req: TfPromptReq, u=Depends(current_user)):
|
||||
(sid, u["id"], "tf-prompt", title))
|
||||
else:
|
||||
c.execute("UPDATE chat_sessions SET updated_at=datetime('now') WHERE id=?", (sid,))
|
||||
# Load TF resource reference for context
|
||||
# Load TF resource reference (compact — same as Terraform Agent)
|
||||
tf_ref = _load_tf_resource_reference()
|
||||
system_with_ref = TFP_SYSTEM_PROMPT
|
||||
if tf_ref:
|
||||
system_with_ref += f"\n\n### Referência de Recursos OCI Terraform Disponíveis\nUse esta referência para garantir que os recursos mencionados no prompt existam no provider OCI:\n\n{tf_ref}"
|
||||
sections = []
|
||||
for line in tf_ref.split('\n'):
|
||||
if line.startswith('## All Resource Types'):
|
||||
break
|
||||
sections.append(line)
|
||||
ref_compact = '\n'.join(sections).strip()
|
||||
if ref_compact:
|
||||
system_with_ref += "\n\n### Referência de Recursos OCI Terraform (gerado do provider schema)\n" + \
|
||||
"Use EXATAMENTE estes nomes de resource types. Se o recurso não estiver nesta lista, ele NÃO EXISTE no provider.\n\n" + \
|
||||
ref_compact
|
||||
|
||||
# Detect resource types from user message + history and fetch official docs
|
||||
detect_text = req.message
|
||||
if req.history:
|
||||
for h in req.history[-3:]:
|
||||
detect_text += "\n" + (h.get("content", "") or "")
|
||||
resource_types = _detect_tf_resource_types(detect_text)
|
||||
if resource_types:
|
||||
docs_ctx = _get_tf_docs_for_resources(resource_types)
|
||||
if docs_ctx:
|
||||
system_with_ref += "\n\n" + docs_ctx
|
||||
log.info(f"TF Prompt Generator: injected {len(resource_types)} resource docs ({len(docs_ctx)} chars)")
|
||||
|
||||
gc["system_prompt"] = system_with_ref
|
||||
gc["temperature"] = 0.7
|
||||
gc["max_tokens"] = 4000
|
||||
|
||||
Reference in New Issue
Block a user