forked from diegoecab/oci-deal-accelerator
Improve diagram generator: auto-sizing, DRG placement, edge labels
6 improvements based on Oracle Architecture Center reference diagrams: 1. Edge label offset — labels pushed 15px away from edge midpoint via mxGeometry offset injection, prevents overlap with icons 2. Auto-sizing containers — regions, VCNs, tenancy calculate dimensions from content instead of using fixed sizes (DR region: 540→260px) 3. jettySize=auto on all edges — clean stubs leaving/entering shapes 4. Dual connection merging — duplicate from/to pairs merged into single edge with combined label (e.g., dual FastConnect) 5. DRG placed OUTSIDE VCN, INSIDE region — matches Oracle ref arch pattern where DRG is the central hub between external and VCN 6. container=1;collapsible=0 injected into container XML styles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
# OCI Deal Accelerator — Architecture Diagram Spec
|
# OCI Deal Accelerator — Architecture Diagram Spec
|
||||||
# Demo: PharmaCorp Mexico — ExaCS dual-region with Data Guard
|
# Demo: PharmaCorp Mexico — ExaCS dual-region with Data Guard
|
||||||
# Layout: Left-to-right flow, ref arch style
|
# Prompt: "ExaCS en Queretaro (prod) + Standby con Data Guard en Sao Paulo,
|
||||||
# Mexico DC → FastConnect → Queretaro (DB subnet) → DR Sao Paulo
|
# Hub-Spoke networking con DRG, FastConnect 10Gbps dual desde Mexico City,
|
||||||
|
# bastion en public subnet, WAF para apps web, Vault para TDE keys."
|
||||||
#
|
#
|
||||||
# This example only includes components the user explicitly requested:
|
# REQUESTED by user: ExaCS (prod+DR), DRG, FastConnect dual, Bastion, WAF, Vault
|
||||||
# ExaCS (prod + DR), Hub-Spoke networking with DRG, FastConnect 10Gbps,
|
# TECHNICAL DEPENDENCIES (whitelist): SGW (ExaCS backup), IGW (public subnet)
|
||||||
# Bastion in public subnet.
|
# NOT included: EBS, Monitoring, Logging, Events, FSDR, Object Storage (not requested)
|
||||||
# DRG and Service Gateway are auto-added per technical dependency whitelist.
|
#
|
||||||
# Monitoring, Logging, Events, WAF, Vault, EBS — NOT included (not requested).
|
# Container sizes: omitted to let auto-sizing calculate from content.
|
||||||
|
# DRG: listed in gateways — generator places it OUTSIDE VCN, INSIDE region.
|
||||||
|
|
||||||
title: "PharmaCorp Mexico — ExaCS Migration Architecture"
|
title: "PharmaCorp Mexico — ExaCS Migration Architecture"
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ external:
|
|||||||
label: "Mexico City\nData Center"
|
label: "Mexico City\nData Center"
|
||||||
icon: "user"
|
icon: "user"
|
||||||
x: 30
|
x: 30
|
||||||
y: 320
|
y: 250
|
||||||
w: 60
|
w: 60
|
||||||
h: 70
|
h: 70
|
||||||
|
|
||||||
@@ -26,144 +28,108 @@ tenancy:
|
|||||||
label: "Oracle Cloud Infrastructure"
|
label: "Oracle Cloud Infrastructure"
|
||||||
x: 200
|
x: 200
|
||||||
y: 60
|
y: 60
|
||||||
w: 1650
|
|
||||||
h: 780
|
|
||||||
|
|
||||||
regions:
|
regions:
|
||||||
# ── Primary Region: Queretaro ──
|
# ── Primary Region: Queretaro ──
|
||||||
- id: "region_qro"
|
- id: "region_qro"
|
||||||
label: "Region — Queretaro (Primary)"
|
label: "Region — Queretaro (Primary)"
|
||||||
primary: true
|
primary: true
|
||||||
x: 15
|
|
||||||
y: 40
|
|
||||||
w: 1050
|
|
||||||
h: 720
|
|
||||||
|
|
||||||
vcns:
|
vcns:
|
||||||
- id: "vcn_hub"
|
- id: "vcn_hub"
|
||||||
label: "Hub VCN (10.0.0.0/16)"
|
label: "Hub VCN (10.0.0.0/16)"
|
||||||
x: 15
|
|
||||||
y: 45
|
|
||||||
w: 1020
|
|
||||||
h: 650
|
|
||||||
|
|
||||||
subnets:
|
subnets:
|
||||||
# Public subnet — bastion access
|
# Public subnet — WAF + Bastion (both requested)
|
||||||
- id: "subnet_pub"
|
- id: "subnet_pub"
|
||||||
label: "Public Subnet — 10.0.0.0/24"
|
label: "Public Subnet — 10.0.0.0/24"
|
||||||
h: 120
|
h: 120
|
||||||
services:
|
services:
|
||||||
|
- id: "waf"
|
||||||
|
label: "WAF\n(Web Apps)"
|
||||||
|
type: "waf"
|
||||||
|
w: 120
|
||||||
|
h: 70
|
||||||
- id: "bastion"
|
- id: "bastion"
|
||||||
label: "Bastion\nService"
|
label: "Bastion\nService"
|
||||||
type: "bastion"
|
type: "bastion"
|
||||||
w: 120
|
w: 120
|
||||||
h: 70
|
h: 70
|
||||||
|
|
||||||
# DB subnet — main workload
|
# DB subnet — ExaCS + Vault (both requested)
|
||||||
- id: "subnet_db"
|
- id: "subnet_db"
|
||||||
label: "Database Subnet (Private) — 10.0.1.0/24"
|
label: "Database Subnet (Private) — 10.0.1.0/24"
|
||||||
h: 150
|
h: 130
|
||||||
services:
|
services:
|
||||||
- id: "exacs_prod"
|
- id: "exacs_prod"
|
||||||
label: "ExaCS X11M\nQuarter Rack\n(Production)"
|
label: "ExaCS X11M\nQuarter Rack\n(Production)"
|
||||||
type: "exacs"
|
type: "exacs"
|
||||||
w: 200
|
w: 200
|
||||||
h: 90
|
h: 80
|
||||||
|
- id: "vault"
|
||||||
|
label: "OCI Vault\nTDE Keys"
|
||||||
|
type: "vault"
|
||||||
|
w: 140
|
||||||
|
h: 80
|
||||||
|
|
||||||
gateways:
|
gateways:
|
||||||
# Auto-added per whitelist: FastConnect requires DRG
|
# DRG — generator places this OUTSIDE VCN, INSIDE region
|
||||||
- id: "drg"
|
- id: "drg"
|
||||||
label: "DRG"
|
label: "DRG"
|
||||||
type: "drg"
|
type: "drg"
|
||||||
w: 90
|
w: 90
|
||||||
h: 60
|
h: 60
|
||||||
# Auto-added per whitelist: ExaCS backup requires SGW
|
# IGW — whitelist: public subnet requires it
|
||||||
|
- id: "igw"
|
||||||
|
label: "Internet\nGateway"
|
||||||
|
type: "igw"
|
||||||
|
w: 100
|
||||||
|
h: 60
|
||||||
|
# SGW — whitelist: ExaCS backup requires it
|
||||||
- id: "sgw"
|
- id: "sgw"
|
||||||
label: "Service\nGateway"
|
label: "Service\nGateway"
|
||||||
type: "service_gateway"
|
type: "service_gateway"
|
||||||
w: 100
|
w: 100
|
||||||
h: 60
|
h: 60
|
||||||
|
|
||||||
# ── DR Region: Sao Paulo (right side) ──
|
# ── DR Region: Sao Paulo ──
|
||||||
- id: "region_gru"
|
- id: "region_gru"
|
||||||
label: "Region — São Paulo (DR)"
|
label: "Region — São Paulo (DR)"
|
||||||
x: 1090
|
|
||||||
y: 40
|
|
||||||
w: 540
|
|
||||||
h: 540
|
|
||||||
|
|
||||||
vcns:
|
vcns:
|
||||||
- id: "vcn_dr"
|
- id: "vcn_dr"
|
||||||
label: "DR VCN (10.1.0.0/16)"
|
label: "DR VCN (10.1.0.0/16)"
|
||||||
x: 15
|
|
||||||
y: 45
|
|
||||||
w: 510
|
|
||||||
h: 470
|
|
||||||
|
|
||||||
subnets:
|
subnets:
|
||||||
- id: "subnet_dr_db"
|
- id: "subnet_dr_db"
|
||||||
label: "DB Subnet (Private) — 10.1.1.0/24"
|
label: "DB Subnet (Private) — 10.1.1.0/24"
|
||||||
h: 200
|
h: 130
|
||||||
services:
|
services:
|
||||||
- id: "exacs_dr"
|
- id: "exacs_dr"
|
||||||
label: "ExaCS Standby\n(Data Guard)"
|
label: "ExaCS Standby\n(Data Guard)"
|
||||||
type: "exacs"
|
type: "exacs"
|
||||||
w: 200
|
w: 200
|
||||||
h: 100
|
h: 80
|
||||||
|
|
||||||
# Connections
|
# Connections
|
||||||
connections:
|
connections:
|
||||||
# Entry: DC → DRG (FastConnect)
|
# FastConnect dual from Mexico City → DRG (will be merged into single edge)
|
||||||
- id: "conn_fc"
|
- id: "conn_fc1"
|
||||||
label: "FastConnect 10Gbps\n(Dual Redundant)"
|
label: "FastConnect 10Gbps\n(Dual Redundant)"
|
||||||
type: "network"
|
type: "fastconnect"
|
||||||
from: "mexico_dc"
|
from: "mexico_dc"
|
||||||
to: "drg"
|
to: "drg"
|
||||||
|
|
||||||
# DR: ExaCS Prod → ExaCS Standby (Data Guard)
|
# Vault → ExaCS (TDE keys)
|
||||||
|
- id: "conn_vault"
|
||||||
|
label: "TDE Keys"
|
||||||
|
type: "internal"
|
||||||
|
from: "vault"
|
||||||
|
to: "exacs_prod"
|
||||||
|
|
||||||
|
# Data Guard: Prod → DR
|
||||||
- id: "conn_dg"
|
- id: "conn_dg"
|
||||||
label: "Data Guard\n(Redo Shipping)"
|
label: "Data Guard\n(Redo Shipping)"
|
||||||
type: "data"
|
type: "data"
|
||||||
from: "exacs_prod"
|
from: "exacs_prod"
|
||||||
to: "exacs_dr"
|
to: "exacs_dr"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# OPTIONAL — add if the user approves during pre-generation review:
|
|
||||||
#
|
|
||||||
# Security:
|
|
||||||
# - id: "vault"
|
|
||||||
# label: "OCI Vault\nTDE Keys"
|
|
||||||
# type: "vault"
|
|
||||||
#
|
|
||||||
# - id: "waf"
|
|
||||||
# label: "WAF"
|
|
||||||
# type: "waf"
|
|
||||||
#
|
|
||||||
# Observability:
|
|
||||||
# - id: "monitoring"
|
|
||||||
# label: "OCI Monitoring"
|
|
||||||
# type: "monitoring"
|
|
||||||
#
|
|
||||||
# - id: "logging"
|
|
||||||
# label: "OCI Audit + Logging"
|
|
||||||
# type: "logging"
|
|
||||||
#
|
|
||||||
# - id: "events"
|
|
||||||
# label: "OCI Events + Notifications"
|
|
||||||
# type: "events"
|
|
||||||
#
|
|
||||||
# Application tier (only if customer has app servers):
|
|
||||||
# - id: "ebs_app"
|
|
||||||
# label: "EBS R12 App Tier"
|
|
||||||
# type: "compute"
|
|
||||||
#
|
|
||||||
# DR orchestration:
|
|
||||||
# - id: "fsdr"
|
|
||||||
# label: "Full Stack DR (FSDR)"
|
|
||||||
# type: "fsdr"
|
|
||||||
#
|
|
||||||
# Backup:
|
|
||||||
# - id: "obj_storage"
|
|
||||||
# label: "Object Storage\nRMAN Backups"
|
|
||||||
# type: "object-storage"
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -909,6 +909,7 @@ class OCIDiagramGenerator:
|
|||||||
# - NO elbow=vertical (it conflicts with orthogonal + port constraints)
|
# - NO elbow=vertical (it conflicts with orthogonal + port constraints)
|
||||||
extra_style = (
|
extra_style = (
|
||||||
"edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;"
|
"edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;"
|
||||||
|
"jettySize=auto;"
|
||||||
"jumpStyle=arc;jumpSize=8;"
|
"jumpStyle=arc;jumpSize=8;"
|
||||||
"endFill=0;startFill=0;"
|
"endFill=0;startFill=0;"
|
||||||
"labelBackgroundColor=#FFFFFF;labelBorderColor=none;"
|
"labelBackgroundColor=#FFFFFF;labelBorderColor=none;"
|
||||||
@@ -1121,6 +1122,57 @@ class OCIDiagramGenerator:
|
|||||||
if pattern in xml:
|
if pattern in xml:
|
||||||
xml = xml.replace(pattern, f'id="{edge_id}" style="{extra_style}')
|
xml = xml.replace(pattern, f'id="{edge_id}" style="{extra_style}')
|
||||||
|
|
||||||
|
# ── Edge label offset injection ──
|
||||||
|
# Push labels away from the edge midpoint to avoid overlapping with
|
||||||
|
# source/target icons. Uses mxGeometry relative positioning:
|
||||||
|
# x: position along edge (-1=source, 0=center, 1=target)
|
||||||
|
# <mxPoint as="offset">: absolute pixel offset from that position
|
||||||
|
# Horizontal edges: push label 15px above the line (offset y=-15)
|
||||||
|
# Vertical edges: push label 15px to the right (offset x=15)
|
||||||
|
for edge_id, (src_id, tgt_id) in self._edge_objects.items():
|
||||||
|
src_pos = self._abs_positions.get(src_id)
|
||||||
|
tgt_pos = self._abs_positions.get(tgt_id)
|
||||||
|
if not src_pos or not tgt_pos:
|
||||||
|
continue
|
||||||
|
src_obj = self._objects.get(src_id)
|
||||||
|
tgt_obj = self._objects.get(tgt_id)
|
||||||
|
if not src_obj or not tgt_obj:
|
||||||
|
continue
|
||||||
|
sx = src_pos[0] + (getattr(src_obj, 'width', 100) or 100) / 2
|
||||||
|
sy = src_pos[1] + (getattr(src_obj, 'height', 60) or 60) / 2
|
||||||
|
tx = tgt_pos[0] + (getattr(tgt_obj, 'width', 100) or 100) / 2
|
||||||
|
ty = tgt_pos[1] + (getattr(tgt_obj, 'height', 60) or 60) / 2
|
||||||
|
dx = tx - sx
|
||||||
|
dy = ty - sy
|
||||||
|
is_horiz = abs(dx) > abs(dy) * 1.5
|
||||||
|
if is_horiz:
|
||||||
|
offset_x, offset_y = 0, -15
|
||||||
|
else:
|
||||||
|
offset_x, offset_y = 15, 0
|
||||||
|
# Find the edge's mxGeometry and inject label offset.
|
||||||
|
# drawpyo emits: <mxGeometry relative="1" as="geometry" />
|
||||||
|
# We replace with expanded form containing offset point.
|
||||||
|
new_geo = (
|
||||||
|
f'<mxGeometry x="-0.2" y="0" relative="1" as="geometry">'
|
||||||
|
f'<mxPoint x="{offset_x}" y="{offset_y}" as="offset" />'
|
||||||
|
f'</mxGeometry>'
|
||||||
|
)
|
||||||
|
# Match both self-closing variants that drawpyo may produce
|
||||||
|
for old_geo in [
|
||||||
|
'<mxGeometry relative="1" as="geometry" />',
|
||||||
|
'<mxGeometry relative="1" as="geometry"/>',
|
||||||
|
]:
|
||||||
|
marker = f'id="{edge_id}"'
|
||||||
|
if marker in xml and old_geo in xml:
|
||||||
|
# Only replace the mxGeometry that belongs to THIS edge
|
||||||
|
# Find the edge cell, then replace its geometry
|
||||||
|
idx = xml.find(marker)
|
||||||
|
if idx >= 0:
|
||||||
|
geo_idx = xml.find(old_geo, idx)
|
||||||
|
if geo_idx >= 0 and geo_idx - idx < 2000: # sanity: within same cell
|
||||||
|
xml = xml[:geo_idx] + new_geo + xml[geo_idx + len(old_geo):]
|
||||||
|
break
|
||||||
|
|
||||||
# Inject raw icon stencil cells before </root>
|
# Inject raw icon stencil cells before </root>
|
||||||
if self._raw_cells:
|
if self._raw_cells:
|
||||||
inject = "\n".join(f" {c}" for c in self._raw_cells)
|
inject = "\n".join(f" {c}" for c in self._raw_cells)
|
||||||
@@ -1132,6 +1184,17 @@ class OCIDiagramGenerator:
|
|||||||
if placeholder in xml:
|
if placeholder in xml:
|
||||||
xml = xml.replace(placeholder, str(obj.id))
|
xml = xml.replace(placeholder, str(obj.id))
|
||||||
|
|
||||||
|
# Inject container=1;collapsible=0 into container styles via XML.
|
||||||
|
# drawpyo doesn't support 'container' as a settable property, so we
|
||||||
|
# append it to the style string in the final XML.
|
||||||
|
for container_marker in ["arcSize=1;", "arcSize=5;", "arcSize=3;",
|
||||||
|
"perimeterSpacing=0;", "spacingTop=4;"]:
|
||||||
|
# Only add to styles that already contain these OCI container markers
|
||||||
|
# and don't already have container=1
|
||||||
|
old = f'{container_marker}"'
|
||||||
|
new = f'{container_marker}container=1;collapsible=0;"'
|
||||||
|
xml = xml.replace(old, new)
|
||||||
|
|
||||||
# Sanitize: no pure white in OCI palette
|
# Sanitize: no pure white in OCI palette
|
||||||
xml = xml.replace('fill="#ffffff"', 'fill="#FCFBFA"')
|
xml = xml.replace('fill="#ffffff"', 'fill="#FCFBFA"')
|
||||||
xml = xml.replace('fill="#FFFFFF"', 'fill="#FCFBFA"')
|
xml = xml.replace('fill="#FFFFFF"', 'fill="#FCFBFA"')
|
||||||
@@ -1221,33 +1284,130 @@ class OCIDiagramGenerator:
|
|||||||
cloud_id, cx, cy, svc_w, svc_h)
|
cloud_id, cx, cy, svc_w, svc_h)
|
||||||
cy += svc_h + 20
|
cy += svc_h + 20
|
||||||
|
|
||||||
# Tenancy
|
# Tenancy — auto-size from regions if w/h not specified in spec
|
||||||
tenancy_spec = spec.get("tenancy", {})
|
tenancy_spec = spec.get("tenancy", {})
|
||||||
tenancy_x = tenancy_spec.get("x", 30)
|
tenancy_x = tenancy_spec.get("x", 30)
|
||||||
tenancy_y = tenancy_spec.get("y", 80)
|
tenancy_y = tenancy_spec.get("y", 80)
|
||||||
tenancy_w = tenancy_spec.get("w", 1850)
|
|
||||||
tenancy_h = tenancy_spec.get("h", 720)
|
# Pre-calculate tenancy size from region content if not explicit
|
||||||
|
if "w" not in tenancy_spec or "h" not in tenancy_spec:
|
||||||
|
regions = tenancy_spec.get("regions", [])
|
||||||
|
max_region_right = 0
|
||||||
|
max_region_bottom = 0
|
||||||
|
region_x_cursor = 15
|
||||||
|
for r in regions:
|
||||||
|
# Estimate region dimensions (will be refined when regions are created)
|
||||||
|
r_vcns = r.get("vcns", [])
|
||||||
|
r_has_drg = any(
|
||||||
|
gw.get("type") in ("drg", "dynamic_routing_gateway")
|
||||||
|
for v in r_vcns for gw in v.get("gateways", [])
|
||||||
|
)
|
||||||
|
drg_w = 120 if r_has_drg else 0
|
||||||
|
# Quick subnet height calc
|
||||||
|
total_sub_h = 0
|
||||||
|
max_sub_w = 300
|
||||||
|
for v in r_vcns:
|
||||||
|
for s in v.get("subnets", []):
|
||||||
|
total_sub_h += s.get("h", 120) + 14
|
||||||
|
svcs = s.get("services", [])
|
||||||
|
row_w = sum(sv.get("w", 150) for sv in svcs) + 16 * max(0, len(svcs) - 1) + 30
|
||||||
|
max_sub_w = max(max_sub_w, row_w)
|
||||||
|
# VCN-internal gateways
|
||||||
|
vcn_gws = [gw for gw in v.get("gateways", [])
|
||||||
|
if gw.get("type") not in ("drg", "dynamic_routing_gateway")]
|
||||||
|
if vcn_gws:
|
||||||
|
max_sub_w += max(gw.get("w", 110) for gw in vcn_gws) + 20
|
||||||
|
|
||||||
|
est_rw = max_sub_w + drg_w + 80
|
||||||
|
est_rh = total_sub_h + 80
|
||||||
|
|
||||||
|
rx_val = r.get("x", region_x_cursor)
|
||||||
|
ry_val = r.get("y", 40)
|
||||||
|
max_region_right = max(max_region_right, rx_val + est_rw)
|
||||||
|
max_region_bottom = max(max_region_bottom, ry_val + est_rh)
|
||||||
|
region_x_cursor = max_region_right + 20
|
||||||
|
|
||||||
|
tenancy_w = tenancy_spec.get("w", max(max_region_right + 30, 600))
|
||||||
|
tenancy_h = tenancy_spec.get("h", max(max_region_bottom + 30, 400))
|
||||||
|
else:
|
||||||
|
tenancy_w = tenancy_spec["w"]
|
||||||
|
tenancy_h = tenancy_spec["h"]
|
||||||
|
|
||||||
gen.add_tenancy(
|
gen.add_tenancy(
|
||||||
"tenancy",
|
"tenancy",
|
||||||
tenancy_spec.get("label", "Oracle Cloud Infrastructure"),
|
tenancy_spec.get("label", "Oracle Cloud Infrastructure"),
|
||||||
x=tenancy_x, y=tenancy_y, w=tenancy_w, h=tenancy_h,
|
x=tenancy_x, y=tenancy_y, w=tenancy_w, h=tenancy_h,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regions
|
# Regions — auto-size containers from content when spec doesn't override
|
||||||
|
region_x_cursor = 15 # tracks right edge for auto-positioning next region
|
||||||
for region in tenancy_spec.get("regions", []):
|
for region in tenancy_spec.get("regions", []):
|
||||||
is_primary = region.get("primary", False)
|
is_primary = region.get("primary", False)
|
||||||
rx = region.get("x", 15 if is_primary else 1195)
|
rx = region.get("x", region_x_cursor)
|
||||||
# ry default 40: ~18px below tenancy label (12pt bold) + comfortable padding
|
|
||||||
ry = region.get("y", 40)
|
ry = region.get("y", 40)
|
||||||
rw = region.get("w", 1140 if is_primary else 640)
|
|
||||||
rh = region.get("h", 670 if is_primary else 480)
|
# ── Pre-calculate content size for auto-sizing ──
|
||||||
|
# Walk all VCNs → subnets → services to compute minimum dimensions
|
||||||
|
region_vcns = region.get("vcns", [])
|
||||||
|
auto_rw, auto_rh = 640 if not is_primary else 1140, 480 if not is_primary else 670
|
||||||
|
|
||||||
|
if region_vcns:
|
||||||
|
# Calculate total height needed from subnets + gateways
|
||||||
|
for vcn in region_vcns:
|
||||||
|
gateways = vcn.get("gateways", [])
|
||||||
|
# Separate DRG from VCN-internal gateways (IGW, NAT, SGW)
|
||||||
|
drg_gateways = [gw for gw in gateways if gw.get("type") in ("drg", "dynamic_routing_gateway")]
|
||||||
|
vcn_gateways = [gw for gw in gateways if gw.get("type") not in ("drg", "dynamic_routing_gateway")]
|
||||||
|
|
||||||
|
gw_lane_w = 0
|
||||||
|
if vcn_gateways:
|
||||||
|
gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20
|
||||||
|
|
||||||
|
subnets = vcn.get("subnets", [])
|
||||||
|
total_subnet_h = sum(s.get("h", 120) for s in subnets) + 14 * max(0, len(subnets) - 1)
|
||||||
|
|
||||||
|
# Max service width across all subnets
|
||||||
|
max_svc_row_w = 0
|
||||||
|
for subnet in subnets:
|
||||||
|
svcs = subnet.get("services", [])
|
||||||
|
row_w = sum(svc.get("w", 150) for svc in svcs) + 16 * max(0, len(svcs) - 1) + 30
|
||||||
|
max_svc_row_w = max(max_svc_row_w, row_w)
|
||||||
|
|
||||||
|
# VCN needs: gw_lane + max(subnet_content_w, specified_w) + margins
|
||||||
|
content_w = gw_lane_w + max(max_svc_row_w, 300) + 40
|
||||||
|
content_h = total_subnet_h + 60 # 32 top + 28 bottom padding
|
||||||
|
|
||||||
|
# Also account for gateway column height
|
||||||
|
if vcn_gateways:
|
||||||
|
total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1) + 64
|
||||||
|
content_h = max(content_h, total_gw_h)
|
||||||
|
|
||||||
|
# Store calculated VCN dimensions for use below
|
||||||
|
vcn["_auto_w"] = content_w
|
||||||
|
vcn["_auto_h"] = content_h
|
||||||
|
vcn["_drg_gateways"] = drg_gateways
|
||||||
|
vcn["_vcn_gateways"] = vcn_gateways
|
||||||
|
|
||||||
|
# Region size = VCN size + margins + DRG lane
|
||||||
|
total_vcn_w = max(v.get("_auto_w", 600) for v in region_vcns)
|
||||||
|
total_vcn_h = max(v.get("_auto_h", 400) for v in region_vcns)
|
||||||
|
# DRG lane on left edge of region (outside VCN)
|
||||||
|
has_drg = any(v.get("_drg_gateways") for v in region_vcns)
|
||||||
|
drg_lane_w = 120 if has_drg else 0
|
||||||
|
auto_rw = total_vcn_w + drg_lane_w + 50 # margins
|
||||||
|
auto_rh = total_vcn_h + 70 # 45 top (label) + 25 bottom
|
||||||
|
|
||||||
|
# Use spec dimensions if explicitly provided, otherwise auto-calculated
|
||||||
|
rw = region.get("w", auto_rw)
|
||||||
|
rh = region.get("h", auto_rh)
|
||||||
|
|
||||||
gen.add_region(
|
gen.add_region(
|
||||||
region["id"],
|
region["id"],
|
||||||
region["label"],
|
region["label"],
|
||||||
"tenancy", rx, ry, rw, rh,
|
"tenancy", rx, ry, rw, rh,
|
||||||
)
|
)
|
||||||
|
# Advance cursor for next region positioning
|
||||||
|
region_x_cursor = rx + rw + 20
|
||||||
|
|
||||||
# Availability Domains (optional)
|
# Availability Domains (optional)
|
||||||
for ad in region.get("availability_domains", []):
|
for ad in region.get("availability_domains", []):
|
||||||
@@ -1257,39 +1417,49 @@ class OCIDiagramGenerator:
|
|||||||
w=ad.get("w", rw - 30), h=ad.get("h", rh - 70),
|
w=ad.get("w", rw - 30), h=ad.get("h", rh - 70),
|
||||||
)
|
)
|
||||||
|
|
||||||
# VCNs
|
# ── DRG placement: OUTSIDE VCN, INSIDE region ──
|
||||||
for vcn in region.get("vcns", []):
|
# Oracle ref arch pattern: DRG sits in the region between external
|
||||||
|
# actors and VCNs, not inside any VCN.
|
||||||
|
has_drg = any(v.get("_drg_gateways") for v in region_vcns)
|
||||||
|
drg_lane_w = 120 if has_drg else 0
|
||||||
|
for vcn in region_vcns:
|
||||||
|
for drg in vcn.get("_drg_gateways", []):
|
||||||
|
drg_id = drg.get("id", gen._next_id())
|
||||||
|
drg_w = drg.get("w", 90)
|
||||||
|
drg_h = drg.get("h", 60)
|
||||||
|
# Position: left edge of region, vertically centered
|
||||||
|
drg_x = 15
|
||||||
|
drg_y = max(45, (rh - drg_h) // 2)
|
||||||
|
label = drg["label"].replace("\\n", "\n")
|
||||||
|
gen.add_service(
|
||||||
|
drg_id, label, drg["type"],
|
||||||
|
region["id"], drg_x, drg_y, drg_w, drg_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
# VCNs — offset right if DRG lane present
|
||||||
|
vcn_offset_x = drg_lane_w + 15
|
||||||
|
for vcn in region_vcns:
|
||||||
vcn_parent = vcn.get("parent", region["id"])
|
vcn_parent = vcn.get("parent", region["id"])
|
||||||
vcn_w = vcn.get("w", rw - 30)
|
vcn_w = vcn.get("w", vcn.get("_auto_w", rw - vcn_offset_x - 15))
|
||||||
vcn_h = vcn.get("h", rh - 70)
|
vcn_h = vcn.get("h", vcn.get("_auto_h", rh - 70))
|
||||||
gen.add_vcn(
|
gen.add_vcn(
|
||||||
vcn["id"],
|
vcn["id"],
|
||||||
f"VCN {vcn['label']}",
|
f"VCN {vcn['label']}",
|
||||||
vcn_parent,
|
vcn_parent,
|
||||||
# y default 45: leaves proper gap below the region label (12pt ≈22px + padding)
|
x=vcn.get("x", vcn_offset_x), y=vcn.get("y", 45),
|
||||||
x=vcn.get("x", 15), y=vcn.get("y", 45),
|
|
||||||
w=vcn_w, h=vcn_h,
|
w=vcn_w, h=vcn_h,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── GATEWAY-AWARE AUTO-LAYOUT ──
|
# ── VCN-internal gateways (IGW, NAT, SGW — NOT DRG) ──
|
||||||
# Step 1: Place gateways on the LEFT EDGE of the VCN.
|
vcn_gateways = vcn.get("_vcn_gateways", [])
|
||||||
# Step 2: Offset subnets RIGHT to avoid overlap with gateways.
|
gw_lane_w = 0
|
||||||
# Step 3: Stack subnets vertically (top-to-bottom).
|
|
||||||
# This matches Oracle ref arch style: gateways on VCN boundary,
|
|
||||||
# subnets fill the remaining width.
|
|
||||||
|
|
||||||
gateways = vcn.get("gateways", [])
|
if vcn_gateways:
|
||||||
gw_lane_w = 0 # width reserved for gateway column
|
gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20
|
||||||
|
total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1)
|
||||||
if gateways:
|
|
||||||
# Calculate gateway lane width (max gateway width + padding)
|
|
||||||
gw_lane_w = max(gw.get("w", 110) for gw in gateways) + 20
|
|
||||||
|
|
||||||
# Place gateways vertically centered in VCN
|
|
||||||
total_gw_h = sum(gw.get("h", 70) for gw in gateways) + 8 * max(0, len(gateways) - 1)
|
|
||||||
gw_start_y = max(32, (vcn_h - total_gw_h) // 2)
|
gw_start_y = max(32, (vcn_h - total_gw_h) // 2)
|
||||||
gy = gw_start_y
|
gy = gw_start_y
|
||||||
for gw in gateways:
|
for gw in vcn_gateways:
|
||||||
gw_id = gw.get("id", gen._next_id())
|
gw_id = gw.get("id", gen._next_id())
|
||||||
gw_w = gw.get("w", 110)
|
gw_w = gw.get("w", 110)
|
||||||
gw_h = gw.get("h", 70)
|
gw_h = gw.get("h", 70)
|
||||||
@@ -1302,9 +1472,9 @@ class OCIDiagramGenerator:
|
|||||||
)
|
)
|
||||||
gy = gw_y + gw_h + 8
|
gy = gw_y + gw_h + 8
|
||||||
|
|
||||||
# Subnet horizontal offset: push right if gateways present
|
# Subnet horizontal offset: push right if VCN gateways present
|
||||||
subnet_x = 8 + gw_lane_w
|
subnet_x = 8 + gw_lane_w
|
||||||
subnet_avail_w = vcn_w - subnet_x - 8 # remaining width for subnets
|
subnet_avail_w = vcn_w - subnet_x - 8
|
||||||
|
|
||||||
# Stack subnets vertically (top-to-bottom)
|
# Stack subnets vertically (top-to-bottom)
|
||||||
subnet_y = 32
|
subnet_y = 32
|
||||||
@@ -1441,8 +1611,28 @@ class OCIDiagramGenerator:
|
|||||||
)
|
)
|
||||||
opx += svc_w + 12
|
opx += svc_w + 12
|
||||||
|
|
||||||
# Connections
|
# Connections — merge duplicate from/to pairs (e.g., dual FastConnect)
|
||||||
for conn in spec.get("connections", []):
|
# into a single edge with combined label
|
||||||
|
connections = spec.get("connections", [])
|
||||||
|
seen_pairs = {} # (from, to) -> index in deduped list
|
||||||
|
deduped = []
|
||||||
|
for conn in connections:
|
||||||
|
pair = (conn["from"], conn["to"])
|
||||||
|
if pair in seen_pairs:
|
||||||
|
# Merge: combine labels
|
||||||
|
existing = deduped[seen_pairs[pair]]
|
||||||
|
old_label = existing.get("label", "")
|
||||||
|
new_label = conn.get("label", "")
|
||||||
|
if old_label and new_label and old_label != new_label:
|
||||||
|
existing["label"] = f"{old_label}\n+ {new_label}"
|
||||||
|
elif new_label and not old_label:
|
||||||
|
existing["label"] = new_label
|
||||||
|
# Upgrade: if either is "Dual", keep it in label
|
||||||
|
else:
|
||||||
|
seen_pairs[pair] = len(deduped)
|
||||||
|
deduped.append(dict(conn))
|
||||||
|
|
||||||
|
for conn in deduped:
|
||||||
conn_id = conn.get("id", gen._next_id())
|
conn_id = conn.get("id", gen._next_id())
|
||||||
gen.add_connection(
|
gen.add_connection(
|
||||||
conn_id, conn.get("label"), conn["type"],
|
conn_id, conn.get("label"), conn["type"],
|
||||||
|
|||||||
Reference in New Issue
Block a user