256 lines
14 KiB
Python
256 lines
14 KiB
Python
"""Собирает .archimate (формат Archi 5) для технологического слоя контура УГМК."""
|
||
import json, pathlib, sys, io
|
||
from xml.sax.saxutils import escape, quoteattr
|
||
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||
HERE = pathlib.Path(__file__).parent
|
||
DATA = json.loads((HERE / "matrix.json").read_text(encoding="utf-8"))
|
||
OUT = pathlib.Path(sys.argv[1])
|
||
|
||
elements = [] # (folder, id, type, name, doc)
|
||
relations = [] # (id, type, src, tgt)
|
||
_rc = [0]
|
||
|
||
def el(folder, eid, etype, name, doc=""):
|
||
elements.append((folder, eid, etype, name, doc)); return eid
|
||
|
||
def rel(rtype, src, tgt):
|
||
_rc[0] += 1
|
||
rid = f"rel-{_rc[0]:03d}"
|
||
relations.append((rid, rtype, src, tgt)); return rid
|
||
|
||
REL_INDEX = {}
|
||
def R(rtype, src, tgt):
|
||
REL_INDEX[(rtype, src, tgt)] = rel(rtype, src, tgt); return REL_INDEX[(rtype, src, tgt)]
|
||
|
||
# ---------------------------------------------------------------- технологический слой
|
||
T = "technology"
|
||
nd_cluster = el(T, "nd-cluster", "Node", "Кластер d8-ugmk-prod (Deckhouse)",
|
||
"clusters/d8-ugmk-prod/kustomization.yaml. GitOps через FluxCD v2.")
|
||
nd_front = el(T, "nd-frontend", "Node", "um-sarex-k8s-frontend-01",
|
||
"Выделенный frontend-узел. К нему прибит ingress-gateway через nodeSelector: "
|
||
"clusters/d8-ugmk-prod/istio-ingress/ingress-controller.yaml")
|
||
nd_s3 = el(T, "nd-s3", "Node", "s3.uralmine.com — объектное хранилище УГМК",
|
||
"Внешнее по отношению к кластеру. MinIO в контуре не разворачивается.")
|
||
nd_nfs = el(T, "nd-nfs", "Node", "NFS-хранилище (StorageClass nfs01)",
|
||
"Единственный storageClass в контуре, 9 упоминаний в манифестах.")
|
||
nd_gitea = el(T, "nd-gitea", "Node", "Gitea 10.222.255.165:3000 (infra/iac)",
|
||
"Источник для Flux: clusters/d8-ugmk-prod/flux-system/gotk-sync.yaml")
|
||
nd_cr = el(T, "nd-cr", "Node", "cr.yandex/crp3ccidau046kdj8g9q",
|
||
"Реестр чартов (OCI) и образов контейнеров.")
|
||
|
||
SS = [
|
||
("ss-istio-gw", "Istio Ingress Gateway", "Deckhouse IngressIstioController «sarex», inlet NodePort 30080/30443. clusters/d8-ugmk-prod/istio-ingress/ingress-controller.yaml"),
|
||
("ss-istio-mesh", "Istio Service Mesh", "chart istio-config-contour 0.1.0. Gateway, VirtualService, RequestAuthentication."),
|
||
("ss-certmanager", "cert-manager", "ClusterIssuer sarex-selfsigned — сертификаты самоподписанные, не Let's Encrypt. Wildcard *.uralmine.com и *.sarex-k8s.uralmine.com."),
|
||
("ss-zitadel", "Zitadel", "chart idp-contour 4.12.13, ns zitadel."),
|
||
("ss-vault", "Vault + Agent Injector", "chart vault-contour 0.2.3, ns vault. Единственный механизм доставки секретов, путь secrets/data/apps/<app>/postgres."),
|
||
("ss-postgres", "PostgreSQL", "chart postgresql-contour 17.0.7, ns postgresql. PVC 20Gi на nfs01."),
|
||
("ss-redis", "Redis", "Разворачивается только в ns pm: apps/pm/d8-ugmk-prod/redis.yaml."),
|
||
("ss-kafka", "Kafka", "chart kafka-contour 0.1.15, ns kafka."),
|
||
("ss-rabbit", "RabbitMQ", "chart rabbitmq-contour 15.2.4, ns rabbitmq. Публикуется наружу на rabbitmq.sarex-k8s.uralmine.com."),
|
||
("ss-camunda", "Camunda Platform", "chart camunda-contour 11.0.14, ns camunda. ВНИМАНИЕ: ни одно приложение контура на неё не сконфигурировано — потребителей нет."),
|
||
]
|
||
for sid, name, doc in SS:
|
||
el(T, sid, "SystemSoftware", name, doc); R("Composition", nd_cluster, sid)
|
||
|
||
CAMUNDA_SUB = ["Zeebe", "Zeebe Gateway", "Operate", "Tasklist", "Optimize", "Identity", "Connectors"]
|
||
cam_ids = []
|
||
for n in CAMUNDA_SUB:
|
||
cid = "ss-cam-" + n.lower().replace(" ", "-")
|
||
el(T, cid, "SystemSoftware", n, "infrastructure/camunda/d8-ugmk-prod/camunda.yaml")
|
||
R("Composition", "ss-camunda", cid); cam_ids.append(cid)
|
||
|
||
ar_charts = el(T, "ar-charts", "Artifact", "Helm-чарты *-contour (OCI)")
|
||
ar_images = el(T, "ar-images", "Artifact", "Образы контейнеров")
|
||
R("Composition", nd_cr, ar_charts); R("Composition", nd_cr, ar_images)
|
||
|
||
# технологические сервисы: id -> (имя, кто реализует, ключ в матрице)
|
||
TS = [
|
||
("ts-ingress", "Маршрутизация HTTPS-трафика", ["ss-istio-gw", "ss-istio-mesh"], "istio"),
|
||
("ts-tls", "Выпуск TLS-сертификатов", ["ss-certmanager"], None),
|
||
("ts-oidc", "Аутентификация OIDC", ["ss-zitadel"], "oidc"),
|
||
("ts-secrets", "Управление секретами", ["ss-vault"], "vault"),
|
||
("ts-rdbms", "Реляционное хранилище", ["ss-postgres"], "postgres"),
|
||
("ts-cache", "Кэш", ["ss-redis"], "redis"),
|
||
("ts-events", "Событийная шина", ["ss-kafka"], "kafka"),
|
||
("ts-amqp", "Очередь сообщений AMQP", ["ss-rabbit"], "rabbitmq"),
|
||
("ts-bpmn", "Оркестрация процессов BPMN", ["ss-camunda"], "camunda"),
|
||
("ts-object", "Объектное хранилище", ["nd-s3"], "s3"),
|
||
]
|
||
for tid, name, realizers, _ in TS:
|
||
el(T, tid, "TechnologyService", name)
|
||
for r in realizers:
|
||
R("Realization", r, tid)
|
||
|
||
R("Assignment", nd_front, "ss-istio-gw")
|
||
R("Serving", "ts-tls", "ss-istio-gw")
|
||
R("Serving", nd_nfs, "ss-postgres")
|
||
R("Serving", nd_gitea, nd_cluster)
|
||
R("Serving", nd_cr, nd_cluster)
|
||
|
||
# ---------------------------------------------------------------- прикладной слой
|
||
app_id = {a: "ac-" + a for a in DATA["apps"]}
|
||
for a in DATA["apps"]:
|
||
el("application", app_id[a], "ApplicationComponent", a, f"apps/{a}/d8-ugmk-prod/")
|
||
|
||
consumers = {}
|
||
for tid, _, _, key in TS:
|
||
if key is None:
|
||
consumers[tid] = []; continue
|
||
if key == "istio":
|
||
lst = [a for a in DATA["apps"] if a in DATA["exposed"]]
|
||
else:
|
||
lst = [a for a in DATA["apps"] if DATA["matrix"][a][key]]
|
||
consumers[tid] = lst
|
||
for a in lst:
|
||
R("Serving", tid, app_id[a])
|
||
|
||
# ---------------------------------------------------------------- представления
|
||
views = []
|
||
|
||
def view(name, doc=""):
|
||
v = {"id": f"view-{len(views)+1}", "name": name, "doc": doc, "objects": [], "conns": []}
|
||
views.append(v); return v
|
||
|
||
_oc = [0]
|
||
def obj(v, eid, x, y, w, h, parent=None):
|
||
_oc[0] += 1
|
||
o = {"id": f"do-{_oc[0]:04d}", "el": eid, "b": (x, y, w, h), "children": [], "src": [], "tgt": []}
|
||
(parent["children"] if parent else v["objects"]).append(o)
|
||
return o
|
||
|
||
def conn(v, rtype, so, to):
|
||
key = (rtype, so["el"], to["el"])
|
||
if key not in REL_INDEX:
|
||
raise KeyError(f"нет отношения в модели: {key}")
|
||
_oc[0] += 1
|
||
cid = f"cn-{_oc[0]:04d}"
|
||
so["src"].append({"id": cid, "rel": REL_INDEX[key], "tgt": to["id"]})
|
||
to["tgt"].append(cid)
|
||
|
||
# --- T1: инфраструктура без приложений
|
||
v1 = view("T1. Технологический слой — контур УГМК",
|
||
"Что развёрнуто в кластере d8-ugmk-prod и какие технологические сервисы это даёт. "
|
||
"Приложения-потребители вынесены в отдельные представления T2–T9.")
|
||
o_ts = {}
|
||
for i, (tid, _, _, _) in enumerate(TS):
|
||
o_ts[tid] = obj(v1, tid, 20 + i * 165, 30, 150, 60)
|
||
|
||
o_cluster = obj(v1, nd_cluster, 20, 140, 1270, 400)
|
||
o_ss = {}
|
||
row1 = ["ss-istio-gw", "ss-istio-mesh", "ss-certmanager", "ss-zitadel", "ss-vault"]
|
||
for i, sid in enumerate(row1):
|
||
o_ss[sid] = obj(v1, sid, 20 + i * 240, 45, 220, 70, o_cluster)
|
||
row2 = ["ss-postgres", "ss-redis", "ss-kafka", "ss-rabbit"]
|
||
for i, sid in enumerate(row2):
|
||
o_ss[sid] = obj(v1, sid, 20 + i * 240, 145, 220, 70, o_cluster)
|
||
o_ss["ss-camunda"] = obj(v1, "ss-camunda", 20, 245, 1230, 130, o_cluster)
|
||
for i, cid in enumerate(cam_ids):
|
||
obj(v1, cid, 10 + i * 174, 50, 162, 60, o_ss["ss-camunda"])
|
||
|
||
o_s3 = obj(v1, nd_s3, 1320, 140, 330, 70)
|
||
o_nfs = obj(v1, nd_nfs, 1320, 230, 330, 70)
|
||
o_gitea = obj(v1, nd_gitea, 1320, 320, 330, 70)
|
||
o_cr = obj(v1, nd_cr, 1320, 410, 330, 130)
|
||
obj(v1, ar_charts, 15, 50, 145, 60, o_cr)
|
||
obj(v1, ar_images, 170, 50, 145, 60, o_cr)
|
||
o_front = obj(v1, nd_front, 20, 570, 300, 60)
|
||
|
||
for tid, _, realizers, _ in TS:
|
||
for r in realizers:
|
||
conn(v1, "Realization", o_ss.get(r) or o_s3, o_ts[tid])
|
||
conn(v1, "Assignment", o_front, o_ss["ss-istio-gw"])
|
||
conn(v1, "Serving", o_ts["ts-tls"], o_ss["ss-istio-gw"])
|
||
conn(v1, "Serving", o_nfs, o_ss["ss-postgres"])
|
||
conn(v1, "Serving", o_gitea, o_cluster)
|
||
conn(v1, "Serving", o_cr, o_cluster)
|
||
|
||
# --- T2..T9: потребители каждого сервиса
|
||
PER_ROW = 6
|
||
for tid, tname, realizers, key in TS:
|
||
lst = consumers[tid]
|
||
if not lst:
|
||
continue
|
||
v = view(f"T{len(views)+1}. Потребители: {tname}",
|
||
f"{len(lst)} из {len(DATA['apps'])} приложений контура. Связи выведены из манифестов "
|
||
f"apps/*/base и apps/*/d8-ugmk-prod.")
|
||
ots = obj(v, tid, 20, 20, 260, 60)
|
||
oss = [obj(v, r, 320 + i * 280, 20, 260, 60) for i, r in enumerate(realizers)]
|
||
for o in oss:
|
||
conn(v, "Realization", o, ots)
|
||
oapps = []
|
||
for i, a in enumerate(lst):
|
||
oapps.append(obj(v, app_id[a], 20 + (i % PER_ROW) * 190, 150 + (i // PER_ROW) * 80, 170, 55))
|
||
for oa in oapps:
|
||
conn(v, "Serving", ots, oa)
|
||
|
||
# ---------------------------------------------------------------- сериализация
|
||
FOLDERS = [("Strategy", "strategy"), ("Business", "business"), ("Application", "application"),
|
||
("Technology & Physical", "technology"), ("Motivation", "motivation"),
|
||
("Implementation & Migration", "implementation_migration"), ("Other", "other"),
|
||
("Relations", "relations"), ("Views", "diagrams")]
|
||
|
||
def emit_obj(o, ind):
|
||
p = " " * ind
|
||
tgt = f' targetConnections={quoteattr(" ".join(o["tgt"]))}' if o["tgt"] else ""
|
||
x, y, w, h = o["b"]
|
||
# Порядок и имена — как их пишет сам Archi: <child>, затем bounds,
|
||
# sourceConnection, вложенные <child>. Имена EClass в метамодели
|
||
# (DiagramModelArchimateObject) в XML не используются.
|
||
s = f'{p}<child xsi:type="archimate:DiagramObject" id="{o["id"]}" archimateElement="{o["el"]}"{tgt}>\n'
|
||
s += f'{p} <bounds x="{x}" y="{y}" width="{w}" height="{h}"/>\n'
|
||
for c in o["src"]:
|
||
s += (f'{p} <sourceConnection xsi:type="archimate:Connection" id="{c["id"]}" '
|
||
f'source="{o["id"]}" target="{c["tgt"]}" relationship="{c["rel"]}"/>\n')
|
||
for c in o["children"]:
|
||
s += emit_obj(c, ind + 2)
|
||
s += f'{p}</child>\n'
|
||
return s
|
||
|
||
out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||
'<archimate:model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||
'xmlns:archimate="http://www.archimatetool.com/archimate" '
|
||
'name="СДЕ Sarex — контур УГМК" id="model-ugmk" version="5.0.0">',
|
||
' <purpose>Технологический слой контура d8-ugmk-prod, выведенный из репозитория infra/iac. '
|
||
'Прикладной слой присутствует только как потребители технологических сервисов — '
|
||
'межсервисные связи приложений моделируются отдельно.</purpose>']
|
||
|
||
for fname, ftype in FOLDERS:
|
||
inner = ""
|
||
if ftype in ("technology", "application"):
|
||
for folder, eid, etype, name, doc in elements:
|
||
if folder != ftype:
|
||
continue
|
||
if doc:
|
||
inner += (f' <element xsi:type="archimate:{etype}" name={quoteattr(name)} id="{eid}">\n'
|
||
f' <documentation>{escape(doc)}</documentation>\n'
|
||
f' </element>\n')
|
||
else:
|
||
inner += f' <element xsi:type="archimate:{etype}" name={quoteattr(name)} id="{eid}"/>\n'
|
||
elif ftype == "relations":
|
||
for rid, rtype, src, tgt in relations:
|
||
inner += (f' <element xsi:type="archimate:{rtype}Relationship" id="{rid}" '
|
||
f'source="{src}" target="{tgt}"/>\n')
|
||
elif ftype == "diagrams":
|
||
for v in views:
|
||
inner += f' <element xsi:type="archimate:ArchimateDiagramModel" name={quoteattr(v["name"])} id="{v["id"]}">\n'
|
||
if v["doc"]:
|
||
inner += f' <documentation>{escape(v["doc"])}</documentation>\n'
|
||
for o in v["objects"]:
|
||
inner += emit_obj(o, 6)
|
||
inner += ' </element>\n'
|
||
if inner:
|
||
out.append(f' <folder name={quoteattr(fname)} id="folder-{ftype}" type="{ftype}">\n{inner} </folder>')
|
||
else:
|
||
out.append(f' <folder name={quoteattr(fname)} id="folder-{ftype}" type="{ftype}"/>')
|
||
|
||
out.append('</archimate:model>')
|
||
OUT.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||
|
||
print(f"записано: {OUT}")
|
||
print(f"элементов: {len(elements)} отношений: {len(relations)} представлений: {len(views)}")
|
||
for v in views:
|
||
n_obj = sum(1 + len(o["children"]) + sum(len(c["children"]) for c in o["children"]) for o in v["objects"])
|
||
print(f" {v['name']} ({n_obj} объектов)")
|