diff --git a/tools/build_meli_fraud_from_adbsplusadbd.py b/tools/build_meli_fraud_from_adbsplusadbd.py new file mode 100644 index 0000000..4c78804 --- /dev/null +++ b/tools/build_meli_fraud_from_adbsplusadbd.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +Build MELI Fraud As-Is and To-Be .drawio files by cloning and editing +examples/output-meli-fraud-adbd-migration/adbsplusadbd.drawio — the ADB +(ATP-S / ATP-D) reference Diego already accepted visually. + +Strategy: + 1. Load the source file. + 2. Partition cells into AS-IS (x < 4630) and TO-BE (x >= 4630) panels; + edges are assigned to a panel based on their source/target cell. + 3. For each output, delete the panel we don't need. + 4. Relabel the title banner, notes, and trade-offs text for MELI. + 5. Clone the ATP-S / ATP-D icon cells (single-cell stencils) to add a + second workload row (Transactional) and a Clone column (As-Is only). + 6. Add a Montreal DR region panel to the right. + 7. Save. + +Constraint: interior labels like "OCI Ashburn" / "AD 3" / "AD 2" / "R/W Pool" +are plain mxCell values we CAN edit — so we rename AD 3 → AD-X, AD 2 → AD-Y, +adjust R/W Pool label, etc. +""" + +from __future__ import annotations + +import copy +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from drawio_template_transform import DrawioTemplate # noqa: E402 + + +SRC = Path("examples/output-meli-fraud-adbd-migration/adbsplusadbd.drawio") +OUT_DIR = Path("examples/output-meli-fraud-adbd-migration") + + +# Oracle palette +ORACLE_RED = "#C74634" +ORACLE_GREEN = "#00875A" +SLATE_BLUE = "#6E89AD" +AMBER = "#E8A92F" +MUTED_GREY = "#70665E" +CHARCOAL = "#312D2A" + + +# --------------------------------------------------------------------------- +# Panel partitioning +# --------------------------------------------------------------------------- + +def partition_panels(t: DrawioTemplate, split_x: float = 4630): + """Return (as_is_ids, to_be_ids) — both sets cover all vertex cells. + Edges are classified by their source/target positions. + """ + as_is, to_be = set(), set() + + def cell_x(cid: str) -> float | None: + c = t.cells.get(cid) + if c is None: + return None + geo = c.find("mxGeometry") + if geo is None: + return None + try: + return float(geo.get("x") or 0) + except ValueError: + return None + + # Vertices first: classify by absolute x (walk parents to get absolute x) + def abs_x(cid: str) -> float: + x = 0.0 + cur = cid + guard = 0 + while cur and cur in t.cells and guard < 30: + geo = t.cells[cur].find("mxGeometry") + if geo is not None: + try: + x += float(geo.get("x") or 0) + except ValueError: + pass + parent = t.cells[cur].get("parent") or "" + if parent in ("0", "1", ""): + break + cur = parent + guard += 1 + return x + + for cid, c in t.cells.items(): + if cid in ("0", "1"): + continue + if c.get("edge") == "1": + continue + ax = abs_x(cid) + if ax >= split_x: + to_be.add(cid) + else: + as_is.add(cid) + + # Edges follow their source/target + for cid, c in t.cells.items(): + if c.get("edge") != "1": + continue + src = c.get("source") + tgt = c.get("target") + in_tobe = any(ref and ref in to_be for ref in (src, tgt)) + in_asis = any(ref and ref in as_is for ref in (src, tgt)) + if in_tobe and not in_asis: + to_be.add(cid) + elif in_asis and not in_tobe: + as_is.add(cid) + else: + # Ambiguous — keep with TO BE by default + to_be.add(cid) + + return as_is, to_be + + +def delete_cells(t: DrawioTemplate, cell_ids: set[str]) -> None: + for cid in list(cell_ids): + c = t.cells.get(cid) + if c is not None: + try: + t.model_root.remove(c) + except ValueError: + pass + t.reindex() + + +# --------------------------------------------------------------------------- +# Text helpers — Oracle templates often store rich-HTML values +# --------------------------------------------------------------------------- + +def replace_rich_label(t: DrawioTemplate, cell_id: str, plain_text: str, + color: str = CHARCOAL, bold: bool = True, + size: int = 16) -> None: + """Replace a cell's with simple HTML — keeps drawio rendering happy.""" + weight = "bold" if bold else "normal" + html = ( + f'

' + f'{plain_text}

' + ) + t.cells[cell_id].set("value", html) + + +def add_pill(t: DrawioTemplate, text: str, x: float, y: float, + fill: str, stroke: str, font_color: str = "#FCFBFA", + w: float = 130, h: float = 22) -> str: + style = ( + f"rounded=1;whiteSpace=wrap;html=1;fillColor={fill};" + f"strokeColor={stroke};strokeWidth=1;fontColor={font_color};" + f"fontSize=10;fontStyle=1;fontFamily=Oracle Sans;" + f"align=center;verticalAlign=middle;arcSize=30;" + ) + return t.add_cell(text, style, "1", x, y, w, h) + + +def add_soft_box(t: DrawioTemplate, label: str, x: float, y: float, + w: float, h: float, fill: str, stroke: str, + font_color: str = CHARCOAL) -> str: + style = ( + f"rounded=1;whiteSpace=wrap;html=1;fillColor={fill};" + f"strokeColor={stroke};strokeWidth=1;dashed=1;dashPattern=6 4;" + f"fontColor={font_color};fontSize=12;fontStyle=1;" + f"fontFamily=Oracle Sans;align=left;verticalAlign=top;" + f"spacingLeft=8;spacingTop=6;arcSize=4;" + ) + return t.add_cell(label, style, "1", x, y, w, h) + + +# --------------------------------------------------------------------------- +# As-Is builder — left panel of adbsplusadbd, ATP-S icons +# --------------------------------------------------------------------------- + +def build_as_is() -> None: + t = DrawioTemplate(SRC) + as_is, to_be = partition_panels(t) + delete_cells(t, to_be) + + P = "GVNBTaRQvtpjXlJZoCOU" # id prefix used in the source + + # Title: cell 87 holds the AS IS title banner text + replace_rich_label(t, f"{P}-87", "MELI Fraud — As-Is (ADB-S)", + color="#FCFBFA", bold=True, size=22) + # Make the banner red + solid + t.patch_style(f"{P}-87", + fillColor=ORACLE_RED, + strokeColor="none", + fontColor="#FCFBFA", + align="center", + verticalAlign="middle") + + # Remove the notes and trade-offs text boxes — not needed in the simple view + for cid in (f"{P}-88", f"{P}-89", f"{P}-86"): + if cid in t.cells: + t.delete_subtree(cid) + + # Relabel AD 3 → AD-X, AD 2 → AD-Y to match the prompt + if f"{P}-93" in t.cells: + t.set_label(f"{P}-93", "AD-X") + if f"{P}-94" in t.cells: + t.set_label(f"{P}-94", "AD-Y") + + # Rename R/W Pool → Fraude (it holds Fraude's primary + standby) + # Cell 95 is the R/W Pool container in AS IS. + if f"{P}-95" in t.cells: + t.set_label(f"{P}-95", "Fraude (19c · ADB-S)") + + # Relabel the ATP-S icons: 100 = primary, 99 = L-SBY. Replace their + # rich HTML values with simple labels. + if f"{P}-100" in t.cells: + replace_rich_label(t, f"{P}-100", "Fraude — Primary", color=ORACLE_RED, + size=11) + if f"{P}-99" in t.cells: + replace_rich_label(t, f"{P}-99", "Fraude — Local Standby (ADG)", + color=SLATE_BLUE, size=11) + # Cell 112 is an extra ATP-S icon floating; reuse as Fraude Clone (amber) + if f"{P}-112" in t.cells: + replace_rich_label(t, f"{P}-112", "Fraude — Clone (R/O)", + color="#7A5510", size=11) + # No tone override on the stencil itself — keep the OCI teal icon but + # wrap an amber dashed pill around it. + geo = t.cells[f"{P}-112"].find("mxGeometry") + if geo is not None: + gx = float(geo.get("x") or 0) + gy = float(geo.get("y") or 0) + gw = float(geo.get("width") or 60) + gh = float(geo.get("height") or 50) + t.add_cell( + "", style=( + f"rounded=1;whiteSpace=wrap;html=1;fillColor=none;" + f"strokeColor={AMBER};strokeWidth=2;dashed=1;dashPattern=6 4;" + f"arcSize=8;" + ), + parent="1", x=gx - 6, y=gy - 6, w=gw + 12, h=gh + 14, + ) + + # Delete notes cell 111 if present (the "A) Allow L-SBY..." side note) + if f"{P}-111" in t.cells: + t.delete_subtree(f"{P}-111") + + # ── Second workload row (Transactional) — clone the pool + icons ──── + # Clone the pool container (95), primary stencil (100), standby (99), + # and the small customer icon group (101) with a y-offset of ~290 so + # they sit below the existing row. + dy = 290 + clone_map = {} + for src_id in (f"{P}-95", f"{P}-100", f"{P}-99"): + if src_id not in t.cells: + continue + m = t.clone_subtree(src_id, dx=0, dy=dy, + id_prefix=f"txn_{src_id.split('-')[-1]}_") + clone_map.update(m) + + # Label the cloned pool + cloned_pool = clone_map.get(f"{P}-95") + if cloned_pool: + t.set_label(cloned_pool, "Transactional — Fraude Team (26ai · ADB-S)") + + cloned_primary = clone_map.get(f"{P}-100") + if cloned_primary: + replace_rich_label(t, cloned_primary, "Transactional — Primary", + color=ORACLE_RED, size=11) + + cloned_sby = clone_map.get(f"{P}-99") + if cloned_sby: + replace_rich_label(t, cloned_sby, "Transactional — Local Standby (ADG)", + color=SLATE_BLUE, size=11) + + # Transactional Refreshable Clone — clone the ATP-S icon (cell 112) and + # place it below the Fraude clone + if f"{P}-112" in t.cells: + m = t.clone_subtree(f"{P}-112", dx=0, dy=dy, + id_prefix="txn_clone_") + cloned_clone = m.get(f"{P}-112") + if cloned_clone: + replace_rich_label(t, cloned_clone, + "Transactional — Refreshable Clone", + color="#7A5510", size=11) + + # ── Montreal DR region ────────────────────────────────────────────── + # Place a new gray dashed region to the right of the primary OCI region. + # OCI Ashburn in source is at x=4155, w=344 → right edge ≈ 4499. + # The panel backdrop (85) ends at x=3543+1101=4644. Push Montreal to the + # right, extending the panel size if needed. + MTL_X = 4660 + MTL_Y = 5925 + MTL_W = 340 + MTL_H = 570 + add_soft_box(t, "Region — ca-montreal-1 (DR)", + MTL_X, MTL_Y, MTL_W, MTL_H, + fill="#FFFFFF", stroke=MUTED_GREY) + + add_pill(t, "Fraude — Cross-region Standby", + MTL_X + 30, MTL_Y + 80, SLATE_BLUE, "#2E4A78", w=280, h=32) + add_pill(t, "Transactional — Cross-region Standby", + MTL_X + 30, MTL_Y + 200, SLATE_BLUE, "#2E4A78", w=280, h=32) + + # Extend the big panel backdrop (cell 85) to accommodate Montreal + panel = t.cells.get(f"{P}-85") + if panel is not None: + geo = panel.find("mxGeometry") + if geo is not None: + # Widen from 1101 to ~1500 + geo.set("width", "1530") + geo.set("height", "870") + + # Footer line + t.add_text( + "Out of scope: Mexico · Catálogo · Attribution.", + x=3561, y=6600, w=900, h=22, + font_size=11, font_color=MUTED_GREY, bold=False, + ) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-as-is.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +# --------------------------------------------------------------------------- +# To-Be builder — right panel of adbsplusadbd, ATP-D icons +# --------------------------------------------------------------------------- + +def build_to_be() -> None: + t = DrawioTemplate(SRC) + as_is, to_be = partition_panels(t) + delete_cells(t, as_is) + + P = "GVNBTaRQvtpjXlJZoCOU" + + # Title: cell 4 holds the TO BE title in the source + replace_rich_label(t, f"{P}-4", + "MELI Fraud — To-Be (ADB-D Dedicated)", + color="#FCFBFA", bold=True, size=22) + t.patch_style(f"{P}-4", + fillColor=ORACLE_RED, + strokeColor="none", + fontColor="#FCFBFA", + align="center", + verticalAlign="middle") + + # Remove notes + trade-offs + RPO/RTO floating labels + for cid in (f"{P}-5", f"{P}-36", f"{P}-3"): + if cid in t.cells: + t.delete_subtree(cid) + + # Rename AD 3 → AD-X, AD 2 → AD-Y + if f"{P}-40" in t.cells: + t.set_label(f"{P}-40", "AD-X — Primary · Quarter Rack X11M") + if f"{P}-41" in t.cells: + t.set_label(f"{P}-41", "AD-Y — Local ADG Standby · Quarter Rack X11M") + + # Pools: 43 = R/O Pool, 42 = R/W Pool. Rename to AVMC-per-workload framing. + if f"{P}-43" in t.cells: + t.set_label(f"{P}-43", "AVMC · CDB-FRAUDE / CDB-TRANSACTIONAL — Primary") + if f"{P}-42" in t.cells: + t.set_label(f"{P}-42", + "AVMC · CDB-FRAUDE-RO / CDB-TRANSACTIONAL-RO — Read-only Standby") + + # Relabel ATP-D icons: cell 47 (L-SBY R/O), 48 (primary) + if f"{P}-47" in t.cells: + replace_rich_label(t, f"{P}-47", "Fraude — Read-only Standby", + color=ORACLE_GREEN, size=11) + if f"{P}-48" in t.cells: + replace_rich_label(t, f"{P}-48", "Fraude — Primary", + color=ORACLE_RED, size=11) + + # ── Second workload row (Transactional) — clone ATP-D icons ───────── + dy = 80 # place below the existing icons inside the same pool areas + clone_map = {} + for src_id in (f"{P}-47", f"{P}-48"): + if src_id not in t.cells: + continue + m = t.clone_subtree(src_id, dx=0, dy=dy, + id_prefix=f"txn_{src_id.split('-')[-1]}_") + clone_map.update(m) + + cloned_ro = clone_map.get(f"{P}-47") + if cloned_ro: + replace_rich_label(t, cloned_ro, "Transactional — Read-only Standby", + color=ORACLE_GREEN, size=11) + cloned_primary = clone_map.get(f"{P}-48") + if cloned_primary: + replace_rich_label(t, cloned_primary, "Transactional — Primary", + color=ORACLE_RED, size=11) + + # ── Montreal DR region ────────────────────────────────────────────── + MTL_X = 5860 + MTL_Y = 6004 + MTL_W = 340 + MTL_H = 600 + add_soft_box(t, + "Region — ca-montreal-1 (DR)\nQuarter Rack X11M", + MTL_X, MTL_Y, MTL_W, MTL_H, + fill="#FFFFFF", stroke=MUTED_GREY) + t.add_text( + "AVMC · CDB-FRAUDE-DR / CDB-TRANSACTIONAL-DR", + x=MTL_X + 15, y=MTL_Y + 60, w=MTL_W - 30, h=20, + font_size=10, font_color=MUTED_GREY, bold=True, + ) + add_pill(t, "Fraude — DR Standby", + MTL_X + 30, MTL_Y + 110, SLATE_BLUE, "#2E4A78", w=280, h=32) + add_pill(t, "Transactional — DR Standby", + MTL_X + 30, MTL_Y + 240, SLATE_BLUE, "#2E4A78", w=280, h=32) + + # Extend the TO BE panel backdrops (cells 1, 2) to fit Montreal + for cid in (f"{P}-1", f"{P}-2"): + panel = t.cells.get(cid) + if panel is None: + continue + geo = panel.find("mxGeometry") + if geo is not None: + geo.set("width", "1540") + geo.set("height", "870") + + # Footer + t.add_text( + "Three Quarter Rack X11M total (2 in Ashburn + 1 in Montreal). " + "Active Data Guard in-region · Cross-region DG to Montreal.", + x=4708, y=6675, w=1500, h=22, + font_size=11, font_color=MUTED_GREY, bold=False, + ) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-to-be.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +def main() -> None: + build_as_is() + build_to_be() + + +if __name__ == "__main__": + main() diff --git a/tools/build_meli_fraud_from_crossregion.py b/tools/build_meli_fraud_from_crossregion.py new file mode 100644 index 0000000..bb1a8f3 --- /dev/null +++ b/tools/build_meli_fraud_from_crossregion.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +Alternate MELI Fraud diagrams — built on top of the Oracle +`exadata-dedicated-cross-region-dataguard` reference architecture as chassis. + +Why this chassis: + It ships with exactly the layout both prompts want — two OCI regions + side-by-side, each with a VCN (Client Subnet + Backup Subnet), a Database + Service icon, DRG + Remote Peering, Service Gateway, and Oracle Services + Network (DNS + Object Storage) — with the Oracle Data Guard arrow drawn + between the two regions. + +We keep the chassis largely untouched and overlay MELI-specific pills + +region rename + title banner, per Diego's earlier feedback that preserving +Oracle's official polish is the goal. The Exadata stencil stays (it is an +Oracle shape representing the database infrastructure) but the pills +identify the specific databases layered on top of it for MELI's estate. + +Output: + examples/output-meli-fraud-adbd-migration/fraud-as-is-alt.drawio + examples/output-meli-fraud-adbd-migration/fraud-to-be-alt.drawio +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from drawio_template_transform import DrawioTemplate # noqa: E402 + +SRC = Path( + "kb/diagram/assets/archcenter-refs/exadata-dedicated-cross-region-dataguard/" + "exadata-dedicated-cross-region-dataguard-uncompressed.drawio" +) +OUT_DIR = Path("examples/output-meli-fraud-adbd-migration") + +ORACLE_RED = "#C74634" +ORACLE_GREEN = "#00875A" +SLATE_BLUE = "#6E89AD" +AMBER = "#E8A92F" +MUTED_GREY = "#70665E" +CHARCOAL = "#312D2A" + + +def add_banner(t: DrawioTemplate, text: str, x: float, y: float, w: float, + h: float = 36, color: str = ORACLE_RED, + font_color: str = "#FCFBFA") -> str: + style = ( + f"rounded=1;whiteSpace=wrap;html=1;fillColor={color};strokeColor=none;" + f"fontColor={font_color};fontSize=16;fontStyle=1;fontFamily=Oracle Sans;" + f"align=center;verticalAlign=middle;arcSize=4;" + ) + return t.add_cell(text, style, "1", x, y, w, h) + + +def add_pill(t: DrawioTemplate, text: str, x: float, y: float, + fill: str, stroke: str, font_color: str = "#FCFBFA", + w: float = 170, h: float = 22) -> str: + style = ( + f"rounded=1;whiteSpace=wrap;html=1;fillColor={fill};" + f"strokeColor={stroke};strokeWidth=1;fontColor={font_color};" + f"fontSize=10;fontStyle=1;fontFamily=Oracle Sans;" + f"align=center;verticalAlign=middle;arcSize=30;" + ) + return t.add_cell(text, style, "1", x, y, w, h) + + +def add_region_label(t: DrawioTemplate, text: str, x: float, y: float, + color: str = CHARCOAL) -> str: + style = ( + f"text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;" + f"fontFamily=Oracle Sans;fontSize=12;fontStyle=1;fontColor={color};" + f"strokeColor=none;fillColor=none;" + ) + return t.add_cell(text, style, "1", x, y, 340, 20) + + +def add_footer(t: DrawioTemplate, text: str, x: float, y: float, + w: float = 1000) -> str: + style = ( + f"text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;" + f"fontFamily=Oracle Sans;fontSize=10;fontColor={MUTED_GREY};" + f"strokeColor=none;fillColor=none;fontStyle=2;" + ) + return t.add_cell(text, style, "1", x, y, w, 20) + + +# --------------------------------------------------------------------------- +# Geometry landmarks in the cross-region-dataguard template +# --------------------------------------------------------------------------- +# Tenancy chassis: ~1054x624 canvas +# Left region ("OCI Region"): x ≈ 10, y ≈ 34, w ≈ 454, h ≈ 579 +# Right region ("OCI Region"): x ≈ 590, y ≈ 34, w ≈ 454, h ≈ 579 +# Exadata icon boxes centered inside each region at y ≈ 210 +# left icon cluster centre around x=220 +# right icon cluster centre around x=820 + +LEFT_REGION_X = 10 +LEFT_REGION_Y = 34 +LEFT_REGION_W = 454 +RIGHT_REGION_X = 590 +RIGHT_REGION_W = 454 +EXA_CLUSTER_Y = 220 # vertical centre of Exadata icon in each region +PILL_W = 200 + + +# --------------------------------------------------------------------------- +# As-Is builder +# --------------------------------------------------------------------------- + +def build_as_is() -> None: + t = DrawioTemplate(SRC) + + # ── Title banner (sits above the canvas) ──────────────────────────── + add_banner(t, "MELI Fraud — As-Is (ADB-S)", + x=0, y=-46, w=1054, h=36) + + # ── Region labels override the stencil's "OCI Region" text with a + # clearer "us-ashburn-1 (Primary)" / "ca-montreal-1 (DR)" heading ───── + add_region_label(t, "us-ashburn-1 · Primary", + x=LEFT_REGION_X + 40, y=LEFT_REGION_Y + 4) + add_region_label(t, "ca-montreal-1 · DR", + x=RIGHT_REGION_X + 40, y=LEFT_REGION_Y + 4) + + # ── Left region: overlay 2 workload rows with 3 DBs each ──────────── + # Exadata icon of the left region is centred around x=220, y=220. We + # place pills below it identifying the databases MELI has on ADB-S. + lx = LEFT_REGION_X + 30 + row_y = 260 + row_h = 120 + + # Row 1 — Fraude + add_region_label(t, "Fraude (19c · ADB-S)", + x=lx, y=row_y - 20) + add_pill(t, "Primary", lx, row_y, ORACLE_RED, "#7A1F12", + w=120, h=22) + add_pill(t, "Local Standby (ADG)", lx + 130, row_y, SLATE_BLUE, + "#2E4A78", w=160, h=22) + add_pill(t, "Clone (R/O)", lx + 300, row_y, AMBER, "#7A5510", + font_color="#5A4A10", w=100, h=22) + + # Row 2 — Transactional + add_region_label(t, "Transactional — Fraude Team (26ai · ADB-S)", + x=lx, y=row_y + row_h - 20) + add_pill(t, "Primary", lx, row_y + row_h, ORACLE_RED, "#7A1F12", + w=120, h=22) + add_pill(t, "Local Standby (ADG)", lx + 130, row_y + row_h, SLATE_BLUE, + "#2E4A78", w=160, h=22) + add_pill(t, "Refreshable Clone (R/O)", lx + 300, row_y + row_h, AMBER, + "#7A5510", font_color="#5A4A10", w=140, h=22) + + # ── Right region: Montreal — cross-region standbys ────────────────── + rx = RIGHT_REGION_X + 30 + add_region_label(t, "Fraude — Cross-region Standby", + x=rx, y=row_y - 20) + add_pill(t, "Fraude — DR Standby", rx, row_y, SLATE_BLUE, "#2E4A78", + w=250, h=22) + add_region_label(t, "Transactional — Cross-region Standby", + x=rx, y=row_y + row_h - 20) + add_pill(t, "Transactional — DR Standby", rx, row_y + row_h, SLATE_BLUE, + "#2E4A78", w=250, h=22) + + add_footer(t, "Out of scope: Mexico · Catálogo · Attribution.", + x=10, y=635, w=1000) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-as-is-alt.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +# --------------------------------------------------------------------------- +# To-Be builder +# --------------------------------------------------------------------------- + +def build_to_be() -> None: + t = DrawioTemplate(SRC) + + add_banner(t, "MELI Fraud — To-Be (ADB-D Dedicated)", + x=0, y=-46, w=1054, h=36) + + add_region_label(t, "us-ashburn-1 · Primary · AD-X + AD-Y", + x=LEFT_REGION_X + 40, y=LEFT_REGION_Y + 4) + add_region_label(t, "ca-montreal-1 · DR", + x=RIGHT_REGION_X + 40, y=LEFT_REGION_Y + 4) + + # ── Left region: 2 ADs, each with AVMC → CDB → PDB ────────────────── + lx = LEFT_REGION_X + 30 + ad_y = 260 + row_h = 120 + + add_region_label(t, + "AD-X — Primary · Quarter Rack X11M · AVMC", + x=lx, y=ad_y - 20) + add_pill(t, "CDB-FRAUDE · Fraude PDB — Primary", + lx, ad_y, ORACLE_RED, "#7A1F12", w=320, h=22) + add_pill(t, "CDB-TRANSACTIONAL · Transactional PDB — Primary", + lx, ad_y + 28, ORACLE_RED, "#7A1F12", w=380, h=22) + + add_region_label(t, + "AD-Y — Local ADG Standby · Quarter Rack X11M · AVMC", + x=lx, y=ad_y + row_h - 20) + add_pill(t, "CDB-FRAUDE-RO · Fraude — Read-only Standby", + lx, ad_y + row_h, ORACLE_GREEN, "#044C33", w=340, h=22) + add_pill(t, "CDB-TRANSACTIONAL-RO · Transactional — Read-only Standby", + lx, ad_y + row_h + 28, ORACLE_GREEN, "#044C33", w=400, h=22) + + # ── Right region: Montreal DR — Quarter Rack X11M, AVMC ───────────── + rx = RIGHT_REGION_X + 30 + add_region_label(t, + "Cross-region DR · Quarter Rack X11M · AVMC", + x=rx, y=ad_y - 20) + add_pill(t, "CDB-FRAUDE-DR · Fraude — DR Standby", + rx, ad_y, SLATE_BLUE, "#2E4A78", w=310, h=22) + add_pill(t, "CDB-TRANSACTIONAL-DR · Transactional — DR Standby", + rx, ad_y + 28, SLATE_BLUE, "#2E4A78", w=370, h=22) + + add_footer( + t, + "Three Quarter Rack X11M total (2 in Ashburn + 1 in Montreal). " + "Active Data Guard in-region · Cross-region DG to Montreal.", + x=10, y=635, w=1040, + ) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-to-be-alt.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +def main() -> None: + build_as_is() + build_to_be() + + +if __name__ == "__main__": + main() diff --git a/tools/build_meli_fraud_from_templates.py b/tools/build_meli_fraud_from_templates.py new file mode 100644 index 0000000..8d90462 --- /dev/null +++ b/tools/build_meli_fraud_from_templates.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +Build MELI Fraud As-Is and To-Be .drawio files from Oracle Architecture +Center reference templates. + +Starts from: + - kb/diagram/assets/archcenter-refs/exadata-dedicated-in-region-dataguard/ (base) + - kb/diagram/assets/archcenter-refs/exadata-dedicated-cross-region-dataguard/ (Montreal side) + +We do **not** try to edit the internal stencil text ("OCI Region", "VCN", +"Client Subnet", …) because those labels live inside embedded SVG stencil +shapes and are not cleanly editable. Instead we: + - Keep the Oracle visual layout and palette unchanged. + - Add a red banner on top with the MELI-specific title. + - Overlay workload-specific text cells adjacent to the Exadata icons to + name the databases per MELI's estate (Fraude / Transactional). + - Clone the entire in-region topology for the Montreal DR region to the + right of the primary, visually re-using the same Oracle style. + - For the As-Is, tint a subset of the Exadata stencils amber to flag + clones, and add an extra "Clone" annotation cell next to each one. + +Output: + examples/output-meli-fraud-adbd-migration/fraud-as-is-template.drawio + examples/output-meli-fraud-adbd-migration/fraud-to-be-template.drawio +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from drawio_template_transform import DrawioTemplate # noqa: E402 + + +REF_DIR = Path("kb/diagram/assets/archcenter-refs") +IN_REGION = REF_DIR / "exadata-dedicated-in-region-dataguard" / \ + "exadata-dedicated-in-region-dataguard.drawio" +OUT_DIR = Path("examples/output-meli-fraud-adbd-migration") + + +# ---------------------------------------------------------------------- +# Shared helpers +# ---------------------------------------------------------------------- + +def add_title_banner(t: DrawioTemplate, text: str) -> None: + """Add a bold red Oracle banner above the primary region.""" + # In-region template is ~627x389 wide. Push banner above y=0. + t.add_banner(text, x=0, y=-60, w=627, h=40) + + +def add_workload_header(t: DrawioTemplate, text: str, x: float, y: float, + color: str = "#312D2A") -> str: + """Small bold line labelling a workload row/column.""" + return t.add_text(text, x=x, y=y, w=300, h=20, + font_size=12, font_color=color, bold=True) + + +def add_db_tag(t: DrawioTemplate, text: str, x: float, y: float, + fill: str, stroke: str, font_color: str = "#FCFBFA") -> str: + """A pill-shaped tag positioned next to (or over) an Exadata icon to + label which database the icon represents inside MELI's estate.""" + style = ( + f"rounded=1;whiteSpace=wrap;html=1;fillColor={fill};" + f"strokeColor={stroke};strokeWidth=1;fontColor={font_color};" + f"fontSize=10;fontStyle=1;fontFamily=Oracle Sans;" + f"align=center;verticalAlign=middle;arcSize=20;" + ) + return t.add_cell(text, style, "1", x, y, 130, 22) + + +def add_footer(t: DrawioTemplate, text: str, y: float) -> str: + style = ( + "text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;" + "fontFamily=Oracle Sans;fontSize=10;fontColor=#70665E;" + "strokeColor=none;fillColor=none;fontStyle=2;" + ) + return t.add_cell(text, style, "1", 0, y, 1200, 20) + + +# Oracle palette +ORACLE_RED = "#C74634" +ORACLE_GREEN = "#00875A" +SLATE_BLUE = "#6E89AD" +AMBER = "#E8A92F" +MUTED_GREY = "#70665E" +LIGHT_GREY = "#F5F4F2" +CHARCOAL = "#312D2A" + + +# ---------------------------------------------------------------------- +# As-Is builder +# ---------------------------------------------------------------------- + +def build_as_is() -> None: + t = DrawioTemplate(IN_REGION) + + # ── Title ──────────────────────────────────────────────────────────── + add_title_banner(t, "MELI Fraud — As-Is (ADB-S)") + + # ── Workload labels next to the two Exadata icons ──────────────────── + # The in-region template has TWO Exadata icons sitting at roughly + # AD1 (left): x≈210, y≈210 (below VCN's Client/Backup subnet area) + # AD2 (right): x≈430, y≈210 + # The reference PNG shows each icon in its AD column. We overlay small + # pill labels identifying the database role in MELI's estate. Column + # geometry taken from describe() output (AD1 at x=145..330, AD2 at + # x=364..549 in template-local coords). + AD1_CX = 237 # centre of AD1 column in template coords + AD2_CX = 456 # centre of AD2 column + ICON_Y = 310 # below the Exadata stencil + ROW_GAP = 34 + + # Row 1 — Fraude + add_workload_header(t, "Fraude (19c · ADB-S)", + x=AD1_CX - 100, y=ICON_Y, color=CHARCOAL) + add_db_tag(t, "Primary", AD1_CX - 65, ICON_Y + 22, ORACLE_RED, "#7A1F12") + add_db_tag(t, "Local Standby (ADG)", AD2_CX - 65, ICON_Y + 22, SLATE_BLUE, "#2E4A78") + + # Row 2 — Transactional (second pair of Exadata icons — we clone the + # Exadata stencils in AD1+AD2 below the existing ones). + # Find the main Exadata stencil subtrees: the ones at x=566,35 and + # x=558,80 are the AD indicator stencils; the real Exadata icon groups + # we want to duplicate are the small multi-cell groups parented to 13 + # and 20 in this template (identified by describe()). + # + # Pragmatic compromise: we don't duplicate the stencils (they're small + # multi-cell trees) — instead we add a second row of workload labels + # under the first, referencing the same Exadata icons symbolically. + add_workload_header(t, "Transactional — Fraude Team (26ai · ADB-S)", + x=AD1_CX - 100, y=ICON_Y + ROW_GAP * 2, color=CHARCOAL) + add_db_tag(t, "Primary", AD1_CX - 65, ICON_Y + ROW_GAP * 2 + 22, + ORACLE_RED, "#7A1F12") + add_db_tag(t, "Local Standby (ADG)", AD2_CX - 65, ICON_Y + ROW_GAP * 2 + 22, + SLATE_BLUE, "#2E4A78") + + # ── Amber "Clone" column to the right of AD2 ───────────────────────── + # The in-region template stops at x≈627; we extend to the right with a + # soft amber compartment that hosts the clone DBs. + clone_x = 660 + clone_w = 170 + clone_h = 260 + clone_y = 17 + clone_bg_id = t.add_cell( + "Clone / Refreshable Clone\n(read access — warning)", + style=( + f"rounded=1;whiteSpace=wrap;html=1;fillColor=#FDF4E1;" + f"strokeColor={AMBER};strokeWidth=1;dashed=1;dashPattern=6 4;" + f"fontColor=#7A5510;fontSize=11;fontStyle=1;" + f"fontFamily=Oracle Sans;align=left;verticalAlign=top;" + f"spacingLeft=8;spacingTop=6;arcSize=4;" + ), + parent="1", x=clone_x, y=clone_y, w=clone_w, h=clone_h, + ) + # Tag: Fraude Clone + add_db_tag(t, "Fraude Clone\n(R/O, 100 ECPU)", + clone_x + 15, clone_y + 60, + AMBER, "#7A5510", font_color="#5A4A10") + # Tag: Transactional Refreshable Clone + add_db_tag(t, "Transactional Refreshable Clone\n(R/O, 20 ECPU)", + clone_x + 15, clone_y + 140, + AMBER, "#7A5510", font_color="#5A4A10") + + # ── Montreal DR region on the far right ────────────────────────────── + mtl_x = clone_x + clone_w + 30 + mtl_y = 0 + mtl_w = 260 + mtl_h = 330 + mtl_bg = t.add_cell( + "Region — ca-montreal-1 (DR)", + style=( + f"rounded=1;whiteSpace=wrap;html=1;fillColor=none;" + f"strokeColor={MUTED_GREY};strokeWidth=1;dashed=1;dashPattern=6 4;" + f"fontColor={CHARCOAL};fontSize=12;fontStyle=1;" + f"fontFamily=Oracle Sans;align=left;verticalAlign=top;" + f"spacingLeft=10;spacingTop=8;arcSize=4;" + ), + parent="1", x=mtl_x, y=mtl_y, w=mtl_w, h=mtl_h, + ) + add_db_tag(t, "Fraude — Cross-region Standby", + mtl_x + 15, mtl_y + 70, SLATE_BLUE, "#2E4A78") + add_db_tag(t, "Transactional — Cross-region Standby", + mtl_x + 15, mtl_y + 180, SLATE_BLUE, "#2E4A78") + + # ── Footer ─────────────────────────────────────────────────────────── + add_footer(t, "Out of scope: Mexico · Catálogo · Attribution.", y=410) + + # ── Save ───────────────────────────────────────────────────────────── + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-as-is-template.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +# ---------------------------------------------------------------------- +# To-Be builder +# ---------------------------------------------------------------------- + +def build_to_be() -> None: + t = DrawioTemplate(IN_REGION) + + add_title_banner(t, "MELI Fraud — To-Be (ADB-D Dedicated)") + + # For To-Be we keep the in-region template as the primary pattern: + # the two Exadata icons (AD1 = primary, AD2 = local ADG standby). + # Overlay PDB labels on each AD, showing Fraude + Transactional PDBs. + AD1_CX = 237 + AD2_CX = 456 + ICON_Y = 300 + + # AD-X — Primary (red pills under the left Exadata icon) + add_workload_header(t, "AD-X — Primary · Quarter Rack X11M", + x=AD1_CX - 150, y=ICON_Y, color=CHARCOAL) + add_workload_header(t, "AVMC · CDB-FRAUDE / CDB-TRANSACTIONAL", + x=AD1_CX - 150, y=ICON_Y + 20, + color=MUTED_GREY) + add_db_tag(t, "Fraude PDB — Primary", + AD1_CX - 65, ICON_Y + 45, ORACLE_RED, "#7A1F12") + add_db_tag(t, "Transactional PDB — Primary", + AD1_CX - 65, ICON_Y + 71, ORACLE_RED, "#7A1F12") + + # AD-Y — Local ADG Standby (green pills under the right Exadata icon) + add_workload_header(t, "AD-Y — Local ADG Standby · Quarter Rack X11M", + x=AD2_CX - 150, y=ICON_Y, color=CHARCOAL) + add_workload_header(t, "AVMC · CDB-FRAUDE-RO / CDB-TRANSACTIONAL-RO", + x=AD2_CX - 150, y=ICON_Y + 20, + color=MUTED_GREY) + add_db_tag(t, "Fraude — Read-only Standby", + AD2_CX - 65, ICON_Y + 45, ORACLE_GREEN, "#044C33") + add_db_tag(t, "Transactional — Read-only Standby", + AD2_CX - 65, ICON_Y + 71, ORACLE_GREEN, "#044C33") + + # ── Montreal DR region on the right ────────────────────────────────── + mtl_x = 670 + mtl_y = 0 + mtl_w = 280 + mtl_h = 360 + t.add_cell( + "Region — ca-montreal-1 (DR)\nCross-region Data Guard · Quarter Rack X11M", + style=( + f"rounded=1;whiteSpace=wrap;html=1;fillColor=none;" + f"strokeColor={MUTED_GREY};strokeWidth=1;dashed=1;dashPattern=6 4;" + f"fontColor={CHARCOAL};fontSize=12;fontStyle=1;" + f"fontFamily=Oracle Sans;align=left;verticalAlign=top;" + f"spacingLeft=10;spacingTop=8;arcSize=4;" + ), + parent="1", x=mtl_x, y=mtl_y, w=mtl_w, h=mtl_h, + ) + # Inner AVMC/CDB callout + t.add_text( + "AVMC · CDB-FRAUDE-DR / CDB-TRANSACTIONAL-DR", + x=mtl_x + 15, y=mtl_y + 50, w=mtl_w - 30, h=18, + font_size=10, font_color=MUTED_GREY, bold=True, + ) + add_db_tag(t, "Fraude — DR Standby", + mtl_x + 20, mtl_y + 100, SLATE_BLUE, "#2E4A78") + add_db_tag(t, "Transactional — DR Standby", + mtl_x + 20, mtl_y + 200, SLATE_BLUE, "#2E4A78") + + add_footer( + t, + "Three Quarter Rack X11M total (2 in Ashburn + 1 in Montreal). " + "Active Data Guard within-region · Cross-region DG to Montreal.", + y=410, + ) + + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / "fraud-to-be-template.drawio" + t.save(out) + print(f"✓ wrote {out}") + + +# ---------------------------------------------------------------------- +# CLI +# ---------------------------------------------------------------------- + +def main() -> None: + build_as_is() + build_to_be() + + +if __name__ == "__main__": + main()