La page précédente a tapé des adresses dans trois CLI. C'est la dernière fois : à partir d'ici, le lab est décrit dans NetBox, équipements, interfaces, câbles, adresses et numéros BGP, et tout le reste en dérive. Cette page décide de ce qui a sa place dans la source de vérité, puis écrit un script pynetbox qui crée tout ça, et que tu peux relancer demain sans dégât.
Avant de commencer
$cd $python3 -m venv .venv$source .venv/bin/activate$pip install pynetbox$export NETBOX_TOKEN= # pour ce shell seulement ; un gestionnaire de secrets pour tout ce qui dure$pip show pynetbox | head -1Name: pynetbox
$curl -sf -o /dev/null -w '%{http_code}' -H "Authorization: Token $NETBOX_TOKEN" /api/dcim/sites/200
Quoi modéliser, et quoi laisser dehors
| Va dans NetBox | Reste dehors |
|---|---|
| Sites, équipements, rôles, types, plateformes | Numéros de série des conteneurs, uptime |
| Interfaces, câbles entre elles | Compteurs d'interface, état des liens |
| Préfixes, les /31 et les loopbacks, l'IP primaire | Tables ARP, routes apprises |
| Numéros d'AS et groupes BGP (config context) | État des sessions BGP |
| Un tag qui dit « ceci appartient au lab » | La configuration rendue elle-même |
Le script de seed
import ipaddress, os, reimport pynetboxnb = pynetbox.api("", token=os.environ["NETBOX_TOKEN"])SITE, TAG = "", ""LOOPBACKS = list(ipaddress.ip_network("").hosts())MGMT_LEN = ipaddress.ip_network("").prefixlenASN_BASE = int("")NOS = …DEVICES = {"spine1": ("spine", None, "", LOOPBACKS[0]), "leaf1": ("leaf", ASN_BASE + 1, "", LOOPBACKS[1]), "leaf2": ("leaf", ASN_BASE + 2, "", LOOPBACKS[2])}P = NOS["ports"]LINKS = [("spine1", P[0], "10.1.0.0/31", "leaf1", P[0], "10.1.0.1/31"), ("spine1", P[1], "10.1.0.2/31", "leaf2", P[0], "10.1.0.3/31")]slug = lambda s: re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")def ensure(endpoint, lookup, **fields): obj = endpoint.get(**lookup) return obj if obj is not None else endpoint.create(**fields)tag = ensure(nb.extras.tags, {"slug": TAG}, name=TAG, slug=TAG)site = ensure(nb.dcim.sites, {"slug": slug(SITE)}, name=SITE, slug=slug(SITE), status="active")mfr = ensure(nb.dcim.manufacturers, {"slug": slug(NOS["manufacturer"])}, name=NOS["manufacturer"], slug=slug(NOS["manufacturer"]))dtype = ensure(nb.dcim.device_types, {"slug": NOS["slug"]}, manufacturer=mfr.id, model=NOS["model"], slug=NOS["slug"])plat = ensure(nb.dcim.platforms, {"slug": NOS["platform"][1]}, name=NOS["platform"][0], slug=NOS["platform"][1], manufacturer=mfr.id)roles = {r: ensure(nb.dcim.device_roles, {"slug": r}, name=r, slug=r, color=c) for r, c in (("spine", "2196f3"), ("leaf", "4caf50"))}for p in ("", "", "10.1.0.0/24"): ensure(nb.ipam.prefixes, {"prefix": p}, prefix=p, site=site.id, status="active", tags=[tag.id])def iface(dev, name, **extra): return ensure(nb.dcim.interfaces, {"device_id": dev.id, "name": name}, device=dev.id, name=name, type="1000base-t", **extra)def address(i, addr): return ensure(nb.ipam.ip_addresses, {"address": addr}, address=addr, status="active", assigned_object_type="dcim.interface", assigned_object_id=i.id, tags=[tag.id])devices = {}for name, (role, asn, mgmt, lo) in DEVICES.items(): dev = ensure(nb.dcim.devices, {"name": name, "site_id": site.id}, name=name, site=site.id, role=roles[role].id, device_type=dtype.id, platform=plat.id, status="active", tags=[tag.id]) mgmt_ip = address(iface(dev, NOS["mgmt"], mgmt_only=True), f"{mgmt}/{MGMT_LEN}") address(iface(dev, NOS["loopback"], type="virtual"), f"{lo}/32") if dev.primary_ip4 is None or dev.primary_ip4.id != mgmt_ip.id: dev.update({"primary_ip4": mgmt_ip.id}) if asn and (dev.local_context_data or {}).get("bgp", {}).get("asn") != asn: dev.update({"local_context_data": {"bgp": {"asn": asn}}}) devices[name] = devfor a_dev, a_port, a_addr, b_dev, b_port, b_addr in LINKS: a, b = iface(devices[a_dev], a_port), iface(devices[b_dev], b_port) address(a, a_addr); address(b, b_addr) if a.cable is None: nb.dcim.cables.create(a_terminations=[{"object_type": "dcim.interface", "object_id": a.id}], b_terminations=[{"object_type": "dcim.interface", "object_id": b.id}], status="connected", tags=[tag.id])ensure(nb.extras.config_contexts, {"name": "bgp-spine"}, name="bgp-spine", roles=[roles["spine"].id], data={"bgp": {"asn": ASN_BASE, "peer_group": "leaves"}})ensure(nb.extras.config_contexts, {"name": "bgp-leaf"}, name="bgp-leaf", roles=[roles["leaf"].id], data={"bgp": {"peer_group": "spines"}})print(f"ok: {len(devices)} devices, {len(LINKS)} cables, site {site.name}")- L'URL est une constante du lab ; le jeton vient de l'environnement pour que le fichier puisse être commité.
- Les noms propres à l'OS, montrés plus bas. Tout ce qui suit cette ligne est indépendant du constructeur.
- Par équipement : rôle, son propre AS (aucun pour le spine, dont l'AS vient du rôle), IP de management, loopback prise dans l'ordre dans
. - Le slug de la plateforme est ce que les plugins d'inventaire de la page suivante donnent à l'outil d'automatisation comme nom de driver :
nokia_srlouarista_eos, pas un joli nom. - L'IP primaire est celle à laquelle les plugins d'inventaire se connectent. Elle doit d'abord être assignée à une interface de cet équipement, d'où l'ordre.
- Un config context par équipement : seules les leaves en ont un, avec leur propre AS.
update()est sauté quand la valeur est déjà là, le journal des changements reste silencieux au second passage. - Un câble est créé une fois, vérifié depuis son côté A. Les câbles de NetBox 4 prennent des listes de terminaisons, c'est ainsi qu'on modélise les breakouts et les câbles multi-brins ; ici chaque liste a une interface.
- Les contextes au niveau du rôle : tous les spines partagent l'AS
, toutes les leaves peerent avec le groupespines. Le contexte rendu d'un équipement est la fusion des deux niveaux.
La ligne NOS pour ton OS réseau :
NOS = {"manufacturer": "Nokia", "model": "SR Linux (container)", "slug": "srlinux", "platform": ("Nokia SR Linux", "nokia_srl"), "mgmt": "mgmt0", "loopback": "system0", "ports": ["ethernet-1/1", "ethernet-1/2"]}Le lancer, deux fois
$cd $.venv/bin/python sot/seed.py$.venv/bin/python sot/seed.py$git add sot/seed.py && git commit -m "sot: seed NetBox with the lab"$cd && .venv/bin/python sot/seed.pyok: 3 devices, 2 cables, site
$curl -sf -H "Authorization: Token $NETBOX_TOKEN" '/api/dcim/devices/?tag=' | python3 -c 'import sys, json; print(json.load(sys.stdin)["count"])'3
S'il s'arrête en route
pynetbox remonte le message d'erreur de NetBox lui-même, qui nomme le champ. Les classiques : un 400 sur le type d'équipement veut dire que le slug existe déjà sous un autre fabricant ; un 400 sur une adresse IP veut dire qu'elle est déjà assignée ailleurs (une expérience précédente, peut-être) ; un 403 veut dire que le jeton n'a pas la permission sur ce modèle. Corrige la cause et relance : tout ce qui est déjà créé est retrouvé, pas recréé.
Vérifier le modèle
$api=/api$auth="Authorization: Token $NETBOX_TOKEN"$curl -s -H "$auth" "$api/dcim/devices/?tag=&brief=1" | python3 -m json.tool$curl -s -H "$auth" "$api/dcim/interfaces/?device=spine1&cabled=true" | python3 -m json.tool | grep -E '"name"|"address"'$curl -s -H "$auth" "$api/dcim/devices/?name=spine1" | python3 -c 'import sys, json; print(json.load(sys.stdin)["results"][0]["config_context"])'$curl -sf -H "Authorization: Token $NETBOX_TOKEN" '/api/dcim/devices/?tag=&has_primary_ip=true' | python3 -c 'import sys, json; print(json.load(sys.stdin)["count"])'3
$curl -sf -H "Authorization: Token $NETBOX_TOKEN" '/api/dcim/cables/?tag=' | python3 -c 'import sys, json; print(json.load(sys.stdin)["count"])'2
$curl -sf -H "Authorization: Token $NETBOX_TOKEN" '/api/dcim/devices/?name=spine1' | python3 -c 'import sys, json; print(json.load(sys.stdin)["results"][0]["config_context"]["bgp"]["asn"])'Terminé
NetBox contient maintenant le lab : le site , trois équipements tagués avec chacun une IP primaire, leurs interfaces, deux câbles, les /31, les loopbacks tirées de , et les numéros BGP dans des config contexts. Le script qui a construit tout ça est dans git et se relance à volonté ; à la dernière page, un pipeline de nuit fait exactement ça.
La page suivante transforme ce modèle en configuration : un inventaire lu depuis NetBox, un template par OS réseau, un rendu et un push, jusqu'à ce que BGP soit établi entre le spine et ses leaves.