Trois pages d'outillage et toujours un point faible : un humain le lance, sur un lab qui est, ou pas, dans l'état qu'il croit. Cette page confie ça à la machine, à chaque changement : un pipeline linte le dépôt, déploie un lab neuf sur un runner installé sur la machine de lab, rend et pousse depuis NetBox, vérifie BGP et les pings avec pytest, publie les configs rendues et le rapport de tests, et démonte le lab quoi qu'il arrive. Puis main est protégée, pour que rien n'y arrive que le pipeline n'ait prouvé.
Avant de commencer
$cd $git remote add origin $git branch -M main$git push -u origin main$git -C remote get-url originL'arborescence du dépôt
/├── topology.clab.yml # page 1├── sot/seed.py # page 2├── templates/, configs/ # page 3├── render.py · push.py # page 3 (ou render.yml · push.yml, inventory/, group_vars/)├── tests/test_lab.py # cette page├── Makefile · requirements.txt · .yamllint└── .github/workflows/lab.yml ou .gitlab-ci.yml.RECIPEPREFIX = >VENV = .venv/bin.PHONY: deps lint deploy render push test destroydeps:> python3 -m venv .venv && $(VENV)/pip install -q -r requirements.txtlint:> $(VENV)/yamllint . && $(VENV)/ruff check .deploy:> sudo containerlab deploy -t topology.clab.yml --reconfigurerender:> $(VENV)/python render.pypush:> $(VENV)/python push.py --committest:> $(VENV)/pytest -q tests --junitxml=report.xmldestroy:> sudo containerlab destroy -t topology.clab.yml --cleanup$printf 'report.xml\n' >> .gitignoreLinter en local
extends: defaultrules: line-length: { max: 200 } truthy: { check-keys: false } document-start: disable comments: { min-spaces-from-content: 1 }$make deps$make lint$cd && make lint >/dev/null && echo lint OKlint OK
Un runner sur la machine de lab
$echo "$USER ALL=(ALL) NOPASSWD: /usr/bin/containerlab" | sudo tee /etc/sudoers.d/containerlab$sudo chmod 440 /etc/sudoers.d/containerlabDans le dépôt : Settings → Actions → Runners → New self-hosted runner, Linux x64. Copie les commandes de téléchargement et de configuration de cette page (elles portent un jeton à usage unique), et ajoute le label :
$mkdir -p ~/actions-runner && cd ~/actions-runner$# curl … | tar xzf … ← les deux lignes de l'interface, avec la version du moment$./config.sh --url --token <jeton à usage unique de l'interface> --labels --unattended$sudo ./svc.sh install && sudo ./svc.sh start$systemctl list-units 'actions.runner.*' --no-legend | grep -c running1
Les tests
import ipaddressimport timeimport pytestimport yamlfrom scrapli import ScrapliNODES = yaml.safe_load(open("topology.clab.yml"))["topology"]["nodes"]LOOPBACKS = list(ipaddress.ip_network("").hosts())PEERS = {"spine1": 2, "leaf1": 1, "leaf2": 1}PLATFORM, AUTH, BGP, PING = …def run(node, command): with Scrapli(host=NODES[node]["mgmt-ipv4"], platform=PLATFORM, auth_username=AUTH[0], auth_password=AUTH[1], auth_strict_key=False) as conn: return conn.send_command(command).resultdef eventually(check, tries=12, wait=5): for _ in range(tries): if check(): return True time.sleep(wait) return False@pytest.mark.parametrize("node", list(NODES))def test_bgp_established(node): assert eventually(lambda: run(node, BGP[0]).count(BGP[1]) >= PEERS[node])def test_loopback_ping(): assert "3 received" in run("leaf1", PING.format(dst=LOOPBACKS[2], src=LOOPBACKS[1]))PLATFORM, AUTH = "nokia_srl", ("admin", "NokiaSrl1!")BGP = ("info from state network-instance default protocols bgp neighbor * session-state", "session-state established")PING = "ping -c 3 {dst} -I {src} network-instance default"$make deploy render push$make test$cd && .venv/bin/pytest -q tests 2>/dev/null | tail -1 | grep -o '[0-9]* passed'4 passed
Si le test BGP expire
Relance make test : s'il passe, la fabric convergeait encore, et un tries=24 est la correction. S'il échoue encore, l'état sur le nœud est ce qu'il est : docker exec clab-${LAB_NAME}-spine1 sr_cli 'show network-instance default protocols bgp neighbor', puis la boîte de dépannage de la page précédente. Un test qui n'arrive pas à se connecter du tout (ScrapliAuthenticationFailed, timeouts), c'est une IP de management qui ne correspond pas à la topologie, ou un nœud qui démarre encore : make deploy rend la main avant que SR Linux accepte SSH.
Le pipeline
Dans le dépôt : Settings → Secrets and variables → Actions → New repository secret, nom NETBOX_TOKEN.
name: labon: pull_request: push: branches: [main] schedule: - cron: "17 3 * * *"jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: "3.12" } - run: make deps lint lab: needs: lint runs-on: [self-hosted, ] timeout-minutes: 30 concurrency: { group: lab-, cancel-in-progress: false } env: { NETBOX_TOKEN: "${{ secrets.NETBOX_TOKEN }}" } steps: - uses: actions/checkout@v4 - run: make deps deploy - run: make render push - run: make test - uses: actions/upload-artifact@v4 if: always() with: { name: "lab-${{ github.sha }}", path: "configs/\nreport.xml" } - if: always() run: make destroy$git add -A && git commit -m "ci: lint, lab and tests" && git push$cd && python3 -c 'import yaml; print(sorted(yaml.safe_load(open(".github/workflows/lab.yml"))["jobs"]))'['lab', 'lint']
$gh run list --workflow lab.yml -L 1 --json conclusion -q '.[0].conclusion'success
Protéger main
Settings → Branches → Add branch ruleset (ou Add classic branch protection rule) sur main : Require a pull request before merging, Require status checks to pass avec lint et lab comme checks requis, Do not allow bypassing. En ligne de commande :
$gh api -X PUT repos/{owner}/{repo}/branches/main/protection --input - <<'EOF'${"required_status_checks": {"strict": true, "contexts": ["lint", "lab"]},$ "enforce_admins": true, "required_pull_request_reviews": {"required_approving_review_count": 0},$ "restrictions": null}$EOF$gh api repos/{owner}/{repo}/branches/main/protection -q '.required_status_checks.contexts | sort | join(",")'lab,lint
Terminé
Chaque changement du lab est maintenant prouvé avant d'atterrir : lint sur le runner hébergé, un lab neuf sur avec la configuration rendue depuis , quatre tests, des artefacts, le démontage, et une branche main qui n'accepte que ce qui est passé. Le passage nocturne surveille la dérive de la source de vérité. D'une machine Linux et d'un fichier YAML à la première page, voilà un réseau que tu reconstruis, testes et relis comme du logiciel, et c'est ce que NetDevOps veut dire.
$git checkout -b try-it && sed -i 's/tries=12/tries=24/' tests/test_lab.py && git commit -am "tests: wait longer for BGP" && git push -u origin try-itOuvre la pull request et regarde le runner faire en cinq minutes ce que les trois premières pages ont fait à la main.