#!/usr/bin/env python3 """Devis de configuration switch (VLANs + SVIs + ACLs) du réseau convergé. DÉRIVÉ des nomenclatures `ip-miroir` de toutes les instances fédérées (dossiers frères `../*/plan/nomenclature.yml` ayant `vmid_schema: ip-miroir` + un `index`). Rien n'est saisi à la main : le devis reste synchrone avec le plan. VLAN = 1000 + index×10 + zone (4 chiffres, unique globalement sur le trunk convergé). Sortie : config Cisco IOS-like (à adapter à la plateforme réelle : NX-OS/IOS-XE, HSRP, VRF). """ from __future__ import annotations import argparse import ipaddress import os import re import sys from pathlib import Path import yaml # Dialecte de CLI du commutateur. Cisco exige un masque INVERSE (wildcard 0.0.255.255) # dans les ACL ; Binardat exige un masque NORMAL (255.255.0.0). Le SVI (ip address) prend # un masque normal sur les deux. Reglable via --dialecte ou SETOPS_DIALECTE. # Liste des dialectes : source unique dans `underlay` (propriete du materiel). DIALECTE_SECOURS = "cisco" RACINE = Path(__file__).resolve().parent.parent DOSSIER_INSTANCES = RACINE.parent # Adressage et VLAN : derives du seul index, source unique dans inventory_rules. sys.path.insert(0, str(RACINE / "scripts")) from inventory_rules import passerelle_de, supernet_de, vlan_de # noqa: E402 import underlay as underlay_mod # noqa: E402 (fabric physique, cluster-global, hors tenant) # Reseaux d'administration : SOURCE UNIQUE partagee avec les nftables d'hote et la # frontiere (intrant `nftables_admin_ssh`). On reutilise le resolveur, pas une copie. from resoudre_flux import _sources_admin_ssh # noqa: E402 DIALECTES = underlay_mod.DIALECTES def dialecte_effectif(explicite: str | None = None) -> str: """Drapeau > variable d'environnement > intrant de fabric > secours. L'intrant `underlay.dialecte` est la valeur DECLAREE (reglable depuis le panneau) ; l'environnement reste un surcharge ponctuelle pour un essai. """ return (explicite or os.environ.get("SETOPS_DIALECTE") or underlay_mod.dialecte(underlay_mod.charger()) or DIALECTE_SECOURS) def masque(cidr: int) -> str: return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask) def reseau_wildcard(supernet: str) -> tuple[str, str]: reseau = ipaddress.IPv4Network(supernet, strict=False) return str(reseau.network_address), str(reseau.hostmask) def masque_acl(supernet: str, dialecte: str) -> tuple[str, str]: """Reseau + masque au format ACL du dialecte : wildcard (cisco) ou normal (binardat).""" reseau = ipaddress.IPv4Network(supernet, strict=False) if dialecte == "binardat": return str(reseau.network_address), str(reseau.netmask) return str(reseau.network_address), str(reseau.hostmask) def remarque(dialecte: str, texte: str) -> list[str]: """Commentaire d'ACL selon le dialecte. Binardat n'a pas de 'remark' : on l'omet.""" return [] if dialecte == "binardat" else [f" remark {texte}"] def section_underlay(underlay: dict | None) -> list[str]: """VLANs + SVIs (si passerelle) de la fabric du routeur. Vide si aucun underlay. Seule la fabric du routeur est configuree ici : les autres (stockage jumbo sur ses propres switches, par exemple) ne partagent aucun cable avec celle-ci. Declarer leurs VLAN sur ces switches serait faux, et les mettre dans leurs trunks aussi. """ fabric = underlay_mod.fabric_du_routeur(underlay) reseaux = underlay_mod.reseaux_de_fabric(underlay, fabric) if not reseaux: return [] out = [f"! ----- 0. Underlay — fabric '{fabric}' (cluster-global, hors tenant) -----"] for r in reseaux: out.append(f"vlan {r['vlan']}") out.append(f" name {r['nom']}") if r.get("mtu") and int(r["mtu"]) > 1500: out.append(f" ! jumbo MTU {r['mtu']} — a appliquer sur les ports + bridges Proxmox") for r in reseaux: if r.get("passerelle"): net = ipaddress.ip_network(r["sous_reseau"], strict=False) out.append(f"interface Vlan{r['vlan']}") out.append(f" description underlay {r['nom']}") out.append(f" ip address {r['passerelle']} {net.netmask}") out.append(" no shutdown") # Roster : seuls les hotes de CETTE fabric. Un equipement d'une autre fabric # (switches de stockage, par exemple) n'a rien a faire dans ce devis, meme en # commentaire — c'est une configuration qu'on applique, pas un inventaire. noms_reseaux = {r["nom"] for r in reseaux} for h in underlay_mod.hotes(underlay): if h.get("reseau") in noms_reseaux: out.append(f"! {h.get('nom','?')} = {h.get('ip','?')} ({h.get('reseau','?')})") # Ce que ce devis NE couvre PAS : les fabrics portees par d'autres switches. for autre in underlay_mod.fabriques(underlay): if autre == fabric: continue r_autres = underlay_mod.reseaux_de_fabric(underlay, autre) out.append(f"! HORS PERIMETRE — fabric '{autre}' : " + ", ".join(f"{r['nom']} (VLAN {r['vlan']})" for r in r_autres)) out.append("! Portee par des switches distincts, sans cable commun avec celle-ci :") out.append("! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage.") out.append("!") return out def port_vers(nom_hote: str) -> str: """Marqueur de port physique vers un equipement nomme.""" return f"" def hote_nomme(underlay: dict | None, nom: str | None) -> dict: """L'hote underlay portant ce nom, ou {}.""" return next((h for h in underlay_mod.hotes(underlay) if h.get("nom") == nom), {}) def ports_ou_marqueur(hote: dict, clef: str, marqueur: str) -> list[str]: """Ports declares pour cette nature de lien, sinon le marqueur a completer.""" valeurs = underlay_mod.ports_de(hote).get(clef) or [] if isinstance(valeurs, str): valeurs = [valeurs] return [str(v) for v in valeurs] or [marqueur] def bloc_trunk(port: str, vlans: str, description: str = "", bord: bool = False) -> list[str]: """Un bloc d'interface trunk complet — l'interface se lit d'un seul tenant.""" out = [f"interface {port}"] if description: out.append(f" description {description}") out += [" switchport mode trunk", f" switchport trunk allowed vlan add {vlans}"] if bord: out.append(" spanning-tree portfast trunk") return out def section_rayons(underlay: dict | None, vlans: str) -> list[str]: """Les rayons de l'etoile : un trunk du routeur vers chaque switch d'acces. Ils sont emis A PART du trunk vers les hyperviseurs parce qu'ils n'ont pas le meme statut vis-a-vis du spanning-tree : un rayon porte les BPDU de la fabric et ne doit JAMAIS etre declare en bord de reseau, sinon la protection tombe la ou elle sert. """ acces = underlay_mod.switches_acces(underlay) if not acces: return [] out = ["! ----- 4c. Rayons de l'etoile (vers les switches d'acces) -----", "! /!\\ CES PORTS NE SONT PAS DES PORTS DE BORD. Ils portent les BPDU de la", "! fabric : y appliquer `portfast` desactiverait la protection anti-boucle", "! exactement la ou elle sert. La section 6 ne les declare volontairement pas."] rayons = underlay_mod.ports_de(hote_nomme(underlay, underlay_mod.routeur(underlay))).get("rayons") or {} for h in acces: port = str(rayons.get(h["nom"]) or port_vers(h["nom"])) out += bloc_trunk(port, vlans, f"rayon vers {h['nom']}") return out def _stp_lignes(dialecte: str, mode: str, priorite: int) -> list[str]: """Mode + priorite de pont, dans la forme du dialecte. Cisco IOS parle `rapid-pvst` (RSTP par VLAN) ; les plateformes generiques parlent `rstp` et une priorite globale. NON VERIFIE sur Binardat : a confronter au `show running-config` du materiel, comme les `ip route`. """ if dialecte == "cisco": return [f"spanning-tree mode {'rapid-pvst' if mode == 'rstp' else mode}", f"spanning-tree vlan 1-4094 priority {priorite}"] return [f"spanning-tree mode {mode}", f"spanning-tree priority {priorite}"] def section_stp(underlay: dict | None, dialecte: str) -> list[str]: """Spanning-tree du switch routeur : il est la racine, par construction. En etoile, tous les chemins passent deja par le centre : en faire le pont racine aligne l'arbre sur la topologie physique au lieu de laisser une election arbitraire le decider. Aucun lien redondant n'existe, donc aucune boucle — RSTP reste actif comme filet contre un brassage accidentel, qui provoquerait sinon une tempete de diffusion sur la fabric convergee. """ s = underlay_mod.stp(underlay) if not s: return ["! ----- 6. Spanning-tree -----", "! Non declare (cle `stp` dans underlay.yml). Sur une fabric a plusieurs", "! switches, un brassage accidentel peut alors boucler sans protection.", "!"] mode, topo = str(s.get("mode", "rstp")), str(s.get("topologie", "")) r_nom = underlay_mod.routeur(underlay) out = [f"! ----- 6. Spanning-tree ({mode.upper()}, topologie {topo}) -----", f"! {r_nom} est le CENTRE de l'etoile : tous les chemins passent par lui.", "! On en fait le pont racine plutot que de laisser une election arbitraire", "! le decider — l'arbre logique suit alors le cablage physique.", "! Aucun lien redondant en etoile : RSTP est un filet, pas une necessite."] out += _stp_lignes(dialecte, mode, underlay_mod.STP_PRIORITE_RACINE) out += ["!", "! Les ports de bord sont declares AVEC leur interface (sections 4 et 4b),", "! pas ici : chaque interface se lit d'un seul tenant. Les rayons de la", "! section 4c n'en sont volontairement pas.", "! BPDU guard non emis volontairement : un pont Linux dont le STP serait", "! active enverrait des BPDU et ferait tomber le port cote hyperviseur.", "! Ne l'ajouter qu'apres avoir verifie que ces ponts n'en emettent pas."] out.append("!") return out def partie_acces(underlay: dict | None, tenants: list, vlans: str, dialecte_b: str = DIALECTE_SECOURS) -> list[str]: """Devis des switches d'ACCES : L2 pur, aucun SVI de zone, aucune ACL, aucune route. Sans MLAG, un seul switch route (cf. `underlay.routeur`). Les autres commutent les trames etiquetees et n'ont d'adresse IP que pour leur propre gestion — ils joignent le reste par la passerelle de management, pas par un SVI a eux. """ r_nom = underlay_mod.routeur(underlay) if not r_nom: return [] fabric = underlay_mod.fabric_du_routeur(underlay) mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric) if r.get("passerelle")), None) # UNIQUEMENT les hotes du reseau de management : les autres equipements declares # (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne # doivent recevoir aucune de ces lignes. autres = [h for h in underlay_mod.hotes(underlay) if h.get("nom") != r_nom and mgmt and h.get("reseau") == mgmt["nom"]] if not autres: return [] out = [ "", "! ============================================================", f"! PARTIE B — SWITCHES D'ACCES (L2 pur) : {', '.join(h['nom'] for h in autres)}", f"! Le routage est porte par {r_nom} SEUL (underlay.routeur). Ces switches", "! commutent les trames etiquetees et ne portent AUCUN SVI de zone, AUCUNE", "! ACL, AUCUNE route : dupliquer les SVI creerait autant de conflits d'IP.", "! ============================================================", "configure terminal", "!", "! ----- B1. Memes VLANs (pour commuter les trames etiquetees) -----", ] # Le transit ne relie que le routeur a la frontiere : aucune trame ne le traverse # ici. Le declarer serait un VLAN qui ne verra jamais de trafic. transit = underlay_mod.reseau_transit(underlay) for r in underlay_mod.reseaux_de_fabric(underlay, fabric): if transit and r["vlan"] == transit["vlan"]: continue out += [f"vlan {r['vlan']}", f" name {r['nom']}"] for nom, pfx, n in tenants: for zone in sorted(n["categories"]): out += [f"vlan {vlan_de(n['index'], zone)}", f" name {pfx}{n['index']}-{n['categories'][zone]['libelle']}"] bord = bool(underlay_mod.stp(underlay)) out += ["!", "! ----- B2. Configuration par switch -----", "! /!\\ UN SEUL de ces blocs par machine : celui qui porte son nom. Adresse de", "! gestion ET ports different d'un switch a l'autre — tout coller ecraserait."] net = ipaddress.ip_network(mgmt["sous_reseau"], strict=False) if mgmt else None for h in autres: out += ["!", f"! --- {h['nom']} " + "-" * max(0, 48 - len(h["nom"])), "!"] if net: out += [f"interface Vlan{mgmt['vlan']}", f" description gestion {h['nom']}", f" ip address {h['ip']} {net.netmask}", " no shutdown", f"ip default-gateway {mgmt['passerelle']} ! le routeur {r_nom}"] else: out.append("! Aucun reseau underlay avec passerelle : gestion a definir a la main.") out += ["! Montante vers le routeur — PAS un port de bord : elle porte les BPDU."] montante = str(underlay_mod.ports_de(h).get("montante") or port_vers(r_nom)) out += bloc_trunk(montante, vlans, f"montante vers {r_nom}") out += ["! Ports terminaux vers les hyperviseurs."] for port in ports_ou_marqueur(h, "hyperviseurs", ""): out += bloc_trunk(port, vlans, bord=bord) s = underlay_mod.stp(underlay) if s: mode = str(s.get("mode", "rstp")) out += ["!", f"! ----- B3. Spanning-tree ({mode.upper()}) — sur les deux -----", "! Priorite VOLONTAIREMENT haute : ces switches ne doivent jamais devenir", f"! racine. Le centre de l'etoile, {r_nom}, la garde en toutes circonstances."] out += _stp_lignes(dialecte_b, mode, underlay_mod.STP_PRIORITE_ACCES) out += ["!"] out += ["end", "write memory"] return out def section_frontiere(transit: dict | None, bord: bool = False, ports: list[str] | None = None) -> list[str]: """Port du switch vers le pare-feu de bordure. Porte le VLAN de transit, et lui seul. Distinct du trunk vers Proxmox : les hyperviseurs n'ont aucune interface sur le transit, et le pare-feu n'a rien a faire des VLAN tenants — il route vers eux, il ne les etiquette pas. """ if not transit: return ["! ----- 4b. Port vers la frontiere -----", "! Aucun reseau de transit declare dans underlay.yml (cle 'passerelle_sortie')."] return [ "! ----- 4b. Port vers la frontiere nord/sud -----", f"! Porte le seul VLAN {transit['vlan']} ({transit['nom']}). En lien dedie,", f"! remplacer par : switchport access vlan {transit['vlan']}.", "! Port terminal : rien derriere lui ne participe au spanning-tree.", ] + [ligne for port in (ports or [""]) for ligne in bloc_trunk(port, str(transit["vlan"]), bord=bord)] def section_routes(underlay: dict | None) -> list[str]: """Routes du switch vers la frontiere nord/sud. Vide si aucun transit declare. Deux routes, et il en faut IMPERATIVEMENT deux : - l'ALLER : defaut vers la frontiere, sinon les hotes n'ont aucune sortie ; - le RETOUR : vers les reseaux d'administration, sinon la reponse d'une VM part par la passerelle de management et revient au pare-feu par une autre interface que celle ou l'etat a ete cree — elle est jetee en silence. Symptome : le SVI de zone repond au ping, mais aucun hote derriere lui n'est joignable. Les reseaux d'administration viennent de l'intrant `nftables_admin_ssh` : la meme source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN de la frontiere. Aucune adresse n'est ecrite ici. """ transit = underlay_mod.reseau_transit(underlay) if not transit: return ["! ----- 5. Routes vers la frontiere -----", "! Aucun reseau de transit declare dans underlay.yml (cle 'passerelle_sortie').", "! Sans lui : pas de route par defaut, et pas de retour vers l'administration.", "!"] sortie = transit["passerelle_sortie"] out = ["! ----- 5. Routes vers la frontiere nord/sud -----", "! /!\\ A APPLIQUER EN DERNIER, ET SEULEMENT UNE FOIS LA FRONTIERE VIVANTE.", f"! Ces routes deplacent la sortie du switch — y compris celle de ses propres", f"! reponses — vers {sortie}. Tant que cette adresse ne repond pas, elles", "! coupent l'acces d'administration AU SWITCH LUI-MEME.", "! Prealables : boitier cable, adresse sur le lien de transit, et joignable", f"! depuis le switch (ping {sortie} depuis Vlan{transit['vlan']}).", "! Garder une session console ouverte pendant l'operation.", "!", f"! Transit '{transit['nom']}' (VLAN {transit['vlan']}) : SVI {transit['passerelle']}" f" <-> frontiere {sortie}", "! Aller : sortie generale de la flotte.", f"ip route 0.0.0.0 0.0.0.0 {sortie}"] admin = _sources_admin_ssh() if admin: out.append("! Retour : sans ces routes, les reponses partent par une autre interface") out.append("! que celle ou l'etat a ete cree, et le pare-feu les jette en silence.") for cidr in admin: net = ipaddress.ip_network(cidr, strict=False) out.append(f"ip route {net.network_address} {net.netmask} {sortie}" f" ! administration (intrant nftables_admin_ssh)") else: out.append("! ATTENTION : intrant `nftables_admin_ssh` vide — aucune route de retour") out.append("! vers l'administration ne peut etre derivee. Cf. preuve P24.") out.append("!") return out def prefixe(nom_dossier: str) -> str: base = re.sub(r"^OPS-", "", nom_dossier) base = re.sub(r"-lab$", "", base) return (re.sub(r"[^A-Za-z0-9]", "", base).upper() or "T")[:4] def decouvrir() -> list[tuple[str, str, dict]]: tenants = [] for chemin in sorted(DOSSIER_INSTANCES.glob("*/plan/nomenclature.yml")): n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {} # Un tenant federe = une nomenclature avec un index (l'adressage en decoule). # `federe: false` exclut un bac a sable local (labo) du reseau converge : # il ne partage pas la fabric de production, on ne provisionne pas ses VLAN. if n.get("index") is not None and n.get("categories") and n.get("federe", True): nom = chemin.parent.parent.name tenants.append((nom, prefixe(nom), n)) tenants.sort(key=lambda t: t[2]["index"]) return tenants def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str: dialecte = dialecte_effectif(dialecte) underlay = underlay_mod.charger() r_nom = underlay_mod.routeur(underlay) out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"] out += [ "! ============================================================", "! DEVIS SWITCH — reseau converge multi-tenant (Set-OPS)", "! Genere par scripts/devis_reseau.py depuis les nomenclatures.", "! VLAN = 1000 + index*10 + zone (unique globalement sur le trunk).", "! Underlay (VLAN < 1000) = fabric physique, cluster-global.", ] if r_nom: out += [ "!", f"! PARTIE A — SWITCH ROUTEUR : {r_nom} (underlay.routeur)", "! Sans MLAG, UN SEUL switch route. Cette partie ne va QUE sur lui.", "! Les autres recoivent la partie B (L2 pur), plus bas.", ] else: out += [ "!", "! /!\\ AUCUN ROUTEUR DESIGNE (cle `routeur` dans underlay.yml).", "! Applique tel quel sur plusieurs switches, ce devis duplique les SVI", "! et cree autant de conflits d'adresses. Designer le switch routeur.", ] out += [ "! ============================================================", "!", ] out += section_underlay(underlay) out += ["! ----- 1. VLANs (tenants) -----"] for nom, pfx, n in tenants: out.append(f"! {nom} (index {n['index']})") for zone in sorted(n["categories"]): c = n["categories"][zone] out.append(f"vlan {vlan_de(n['index'], zone)}") out.append(f" name {pfx}{n['index']}-{c['libelle']}") out += ["!", "! ----- 2. Interfaces de routage (SVI = passerelle des hotes) -----"] for nom, pfx, n in tenants: m = masque(int(n.get("cidr_hote", 24))) for zone in sorted(n["categories"]): c = n["categories"][zone] out.append(f"interface Vlan{vlan_de(n['index'], zone)}") out.append(f" description {nom}-{c['libelle']}") out.append(f" ip address {passerelle_de(n['index'], zone)} {m}") out.append(f" ip access-group {pfx}{n['index']}-ISOLATION in") out.append(" no shutdown") out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"] for nom, pfx, n in tenants: reseau, m = masque_acl(supernet_de(n["index"]), dialecte) out.append(f"ip access-list extended {pfx}{n['index']}-ISOLATION") out += remarque(dialecte, f"Intra-tenant {nom} : routage local autorise") out.append(f" permit ip {reseau} {m} {reseau} {m}") for autre_nom, _, autre in tenants: if autre_nom == nom: continue a_reseau, a_m = masque_acl(supernet_de(autre["index"]), dialecte) out += remarque(dialecte, f"Bloquer le tenant {autre_nom}") out.append(f" deny ip {reseau} {m} {a_reseau} {a_m}") # L'underlay est la fabric physique : mgmt des switches, mgmt Proxmox, OOB/IPMI, # iSCSI, Ceph. Le trafic d'un tenant vers ces reseaux est route LOCALEMENT par le # switch : il ne passe jamais par la frontiere, donc il n'est jamais filtre. Sans # ce deny, le `permit any` final l'autorise — une VM atteindrait la console # physique des hyperviseurs. Aucun flux du registre ne vise l'underlay. # TOUTES les fabrics y passent, meme celles portees par d'autres switches : la # regle porte sur l'adresse de DESTINATION, pas sur le cablage. Si un jour un # chemin s'ouvre vers le stockage, il est deja ferme. for r in underlay_mod.reseaux(underlay): u_reseau, u_m = masque_acl(r["sous_reseau"], dialecte) out += remarque(dialecte, f"Bloquer l'underlay {r['nom']} (fabric physique)") out.append(f" deny ip {reseau} {m} {u_reseau} {u_m}") out += remarque(dialecte, "Reste (Internet / inter-tenant controle) -> passerelle OPNsense") out.append(f" permit ip {reseau} {m} any") out += ["!", "! ----- 4. Trunk vers les hyperviseurs -----", "! Ports TERMINAUX : rien derriere eux ne participe au spanning-tree.", "! Les liens vers les autres switches sont en section 4c, pas ici."] transit = underlay_mod.reseau_transit(underlay) # Le VLAN de transit ne va PAS vers Proxmox : aucun hyperviseur n'a d'interface # dessus. Il sort par le port de la frontiere (ci-dessous), et lui seul. vlans_underlay = [str(r["vlan"]) for r in underlay_mod.reseaux_de_fabric( underlay, underlay_mod.fabric_du_routeur(underlay)) if not (transit and r["vlan"] == transit["vlan"])] vlans_tenants = [ str(vlan_de(n["index"], zone)) for _, _, n in tenants for zone in sorted(n["categories"]) ] vlans = ",".join(vlans_underlay + vlans_tenants) routeur_h = hote_nomme(underlay, underlay_mod.routeur(underlay)) bord = bool(underlay_mod.stp(underlay)) for port in ports_ou_marqueur(routeur_h, "hyperviseurs", ""): out += bloc_trunk(port, vlans, bord=bord) out += ["!"] + section_frontiere( transit, bord=bool(underlay_mod.stp(underlay)), ports=ports_ou_marqueur(hote_nomme(underlay, underlay_mod.routeur(underlay)), "frontiere", "")) out += ["!"] + section_rayons(underlay, ",".join(vlans_underlay + vlans_tenants)) out += ["!"] + section_routes(underlay) out += section_stp(underlay, dialecte) out += ["end", "write memory"] # Les switches d'acces : memes VLANs, aucun SVI de zone, aucune ACL, aucune route. out += partie_acces(underlay, tenants, ",".join(vlans_underlay + vlans_tenants), dialecte) return "\n".join(out) def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--dialecte", choices=DIALECTES, default=None, help="CLI du commutateur (defaut : SETOPS_DIALECTE, sinon underlay.dialecte)") args = ap.parse_args() tenants = decouvrir() if not tenants: print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr) sys.exit(1) print(generer(tenants, args.dialecte)) if __name__ == "__main__": main()