<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat — Programmation &gt; Python</title>
  <link href="https://vincent.bernat.ch/fr/blog/atom-programming-python.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/fr/blog#tag-programming-python" rel="alternate"/>
  <id>http://www.luffy.cx/fr/blog/atom-programming-python.xml/</id>
  <updated>2026-09-26T04:52:45Z</updated>

  <entry>
    <title type="html">PKI hors ligne avec 3 YubiKeys et une carte ARM</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2025-pki-hors-ligne-yubikeys" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2025-pki-hors-ligne-yubikeys#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-03-17T07:12:54Z</updated>
    <id>http://www.luffy.cx/fr/blog/2025-pki-hors-ligne-yubikeys.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Une <abbr title="Public Key Infrastructure">PKI</abbr> hors ligne renforce la sécurité en isolant physiquement l’autorité de
certification des menaces réseau. Une <a href="https://www.yubico.com/products/yubikey-5-overview/" title="Discover YubiKey 5">YubiKey</a> est une solution économique
pour stocker un certificat racine. Vous avez également besoin d’un environnement
isolé pour les opérations sur l’autorité de certification racine.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 400px"><span class="lf-media-inner" style="padding-bottom: 86.500%"><img alt="PKI s'appuyant sur un ensemble de 3 YubiKeys : 2 pour l'autorité de&#10;certification racine et 1 pour l'autorité de certification&#10;intermédiaire." src="https://media-bernat-ch.b-cdn.net/images/3-yubikeys@1x.8beb69c4aaa2a2.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/3-yubikeys@1x.8beb69c4aaa2a2.jpg 503w,https://media-bernat-ch.b-cdn.net/images/3-yubikeys@2x.4c6fa0e083e9cc.jpg 800w" sizes="auto, (max-width: 400px) 100vw, 400px" width="400" height="346" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAK1AQMAAAAKYhhvAAAAA1BMVEW0o5wZvltTAAAAW0lEQVR42u3BMQEAAADCoPVPbQlPoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAmwEReAABFX7yMQAAAABJRU5ErkJggg==)"/></span></div><figcaption>PKI hors ligne sauvegardée par 3 YubiKeys</figcaption></figure>
<p>Ce billet décrit un système <abbr title="Public Key Infrastructure">PKI</abbr> hors ligne utilisant les composants suivants :</p>
<ul>
<li>2 YubiKeys pour l’<strong>autorité de certification racine</strong> (avec une validité de
  20 ans) ;</li>
<li>1 YubiKey pour l’<strong>autorité de certification intermédiaire</strong> (avec une
  validité de 5 ans) ;</li>
<li>1 carte <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre Computer Sweet Potato</a> comme <strong>nano-ordinateur isolé</strong>.</li>
</ul>
<p>Si nécessaire, il est possible d’ajouter plus de YubiKeys comme sauvegarde de
l’autorité de certification racine. Ce n’est pas utile pour l’autorité de
certification intermédiaire car vous pouvez en générer une nouvelle si
l’actuelle est détruite.</p>
<div class="toc">
<ul>
<li><a href="#la-partie-logicielle">La partie logicielle</a></li>
<li><a href="#la-partie-materielle">La partie matérielle</a></li>
<li><a href="#lassemblage-nix">L’assemblage Nix</a></li>
</ul>
</div>
<h1 id="la-partie-logicielle">La partie logicielle</h1>
<p><a href="https://github.com/vincentbernat/offline-pki" title="Offline PKI system"><code>offline-pki</code></a> est une petite application Python pour gérer une <abbr title="Public Key Infrastructure">PKI</abbr>
hors ligne. Elle s’appuie sur <a href="https://github.com/Yubico/yubikey-manager" title="Python library for configuring any YubiKey">yubikey-manager</a> pour gérer les YubiKeys et sur
<a href="https://cryptography.io/en/latest/" title="cryptography's documentation">cryptography</a> pour les opérations cryptographiques non exécutées par les
YubiKeys. L’application a certains choix de conception opiniâtres. Notamment, la
cryptographie est codée en dur pour utiliser la <a href="https://en.wikipedia.org/wiki/P-384" title="P-384 on Wikipedia">courbe elliptique NIST
P-384</a>.</p>
<p>La première étape consiste à réinitialiser toutes vos YubiKeys :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>yubikey<span class="w"> </span>reset
<span class="go">This will reset the connected YubiKey. Are you sure? [y/N]: y</span>
<span class="go">New PIN code:</span>
<span class="go">Repeat for confirmation:</span>
<span class="go">New PUK code:</span>
<span class="go">Repeat for confirmation:</span>
<span class="go">New management key ('.' to generate a random one):</span>
<span class="go">WARNING[pki-yubikey] Using random management key: e8ffdce07a4e3bd5c0d803aa3948a9c36cfb86ed5a2d5cf533e97b088ae9e629</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey OTP+FIDO+CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.management] Device config written</span>
<span class="go">INFO[yubikit.piv] PIV application data reset performed</span>
<span class="go">INFO[yubikit.piv] Management key set</span>
<span class="go">INFO[yubikit.piv] New PUK set</span>
<span class="go">INFO[yubikit.piv] New PIN set</span>
<span class="go">INFO[pki-yubikey] YubiKey reset successful!</span>
</code></pre></div>

<p>Ensuite, générez l’autorité de certification racine et créez autant de copies
que vous voulez :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>certificate<span class="w"> </span>root<span class="w"> </span>--permitted<span class="w"> </span>example.com
<span class="go">Management key for Root X:</span>
<span class="go">Plug YubiKey "Root X"...</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.piv] Data written to object slot 0x5fc10a</span>
<span class="go">INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True</span>
<span class="go">INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384</span>
<span class="go">Copy root certificate to another YubiKey? [y/N]: y</span>
<span class="go">Plug YubiKey "Root X"...</span>
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[yubikit.piv] Data written to object slot 0x5fc10a</span>
<span class="go">INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True</span>
<span class="go">INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384</span>
<span class="go">Copy root certificate to another YubiKey? [y/N]: n</span>
</code></pre></div>

<p>Vous pouvez examiner le résultat :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>offline-pki<span class="w"> </span>yubikey<span class="w"> </span>info
<span class="go">INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00</span>
<span class="go">INFO[pki-yubikey] SN: 23854514</span>
<span class="go">INFO[pki-yubikey] Slot 9C (SIGNATURE):</span>
<span class="go">INFO[pki-yubikey]   Private key type: ECCP384</span>
<span class="go">INFO[pki-yubikey]   Public key:</span>
<span class="go">INFO[pki-yubikey]     Algorithm:  secp384r1</span>
<span class="go">INFO[pki-yubikey]     Issuer:     CN=Root CA</span>
<span class="go">INFO[pki-yubikey]     Subject:    CN=Root CA</span>
<span class="go">INFO[pki-yubikey]     Serial:     1</span>
<span class="go">INFO[pki-yubikey]     Not before: 2024-07-05T18:17:19+00:00</span>
<span class="go">INFO[pki-yubikey]     Not after:  2044-06-30T18:17:19+00:00</span>
<span class="go">INFO[pki-yubikey]     PEM:</span>
<span class="go">-----BEGIN CERTIFICATE-----</span>
<span class="go">MIIBcjCB+aADAgECAgEBMAoGCCqGSM49BAMDMBIxEDAOBgNVBAMMB1Jvb3QgQ0Ew</span>
<span class="go">HhcNMjQwNzA1MTgxNzE5WhcNNDQwNjMwMTgxNzE5WjASMRAwDgYDVQQDDAdSb290</span>
<span class="go">IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERg3Vir6cpEtB8Vgo5cAyBTkku/4w</span>
<span class="go">kXvhWlYZysz7+YzTcxIInZV6mpw61o8W+XbxZV6H6+3YHsr/IeigkK04/HJPi6+i</span>
<span class="go">zU5WJHeBJMqjj2No54Nsx6ep4OtNBMa/7T9foyMwITAPBgNVHRMBAf8EBTADAQH/</span>
<span class="go">MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAwNoADBlAjEAwYKy/L8leJyiZSnn</span>
<span class="go">xrY8xv8wkB9HL2TEAI6fC7gNc2bsISKFwMkyAwg+mKFKN2w7AjBRCtZKg4DZ2iUo</span>
<span class="go">6c0BTXC9a3/28V5aydZj6rvx0JqbF/Ln5+RQL6wFMLoPIvCIiCU=</span>
<span class="go">-----END CERTIFICATE-----</span>
</code></pre></div>

<p>Ensuite, vous pouvez créer un certificat intermédiaire avec <code>offline-pki yubikey
intermediate</code> et l’utiliser pour signer des certificats en fournissant leur <abbr title="Certificate Signing Request">CSR</abbr>
à <code>offline-pki certificate sign</code>. Soyez prudent et inspectez la <abbr title="Certificate Signing Request">CSR</abbr> avant de la
signer, car seul le nom du sujet peut être remplacé. Consultez la
<a href="https://github.com/vincentbernat/offline-pki?tab=readme-ov-file#operations">documentation</a> pour plus de détails. Obtenez les options disponibles en
utilisant l’option <code>--help</code>.</p>
<h1 id="la-partie-materielle">La partie matérielle</h1>
<p>Pour garantir que les opérations sur les autorités de certification racine et
intermédiaire sont isolées du réseau, une solution économique consiste à
utiliser une carte ARM64. Le <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre Computer Sweet Potato</a> est une
alternative plus ouverte au célèbre Raspberry Pi<sup id="fnref:root"><a class="footnote-ref" href="#fn:root">1</a></sup>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 800px"><span class="lf-media-inner" style="padding-bottom: 70.000%"><img alt="Ordinateur monocarte Libre Computer Sweet Potato s'appuyant sur le SOC Amlogic&#10;S905X" src="https://media-bernat-ch.b-cdn.net/images/sweet-potato@1x.34ce59fcbcf47f.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/sweet-potato@1x.34ce59fcbcf47f.jpg 1006w,https://media-bernat-ch.b-cdn.net/images/sweet-potato@2x.e575728a8cf197.jpg 1600w" sizes="auto, (max-width: 800px) 100vw, 800px" width="800" height="560" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABkAAAARhAQMAAABArdteAAAAA1BMVEXGnW8rul+2AAAA8UlEQVR42u3BMQEAAADCoPVPbQo/oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuBhwVgABAgn2ogAAAABJRU5ErkJggg==)"/></span></div><figcaption>SBC Libre Computer Sweet Potato, alimenté par le SOC AML-S905X</figcaption></figure>
<p>J’interagis avec lui via un <a href="https://www.amazon.fr/dp/B07WX2DSVB" title="Convertisseur USB vers série TTL sur Amazon">convertisseur USB vers série</a> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>tio<span class="w"> </span>/dev/ttyUSB0
<span class="go">[16:40:44.546] tio v3.7</span>
<span class="go">[16:40:44.546] Press ctrl-t q to quit</span>
<span class="go">[16:40:44.555] Connected to /dev/ttyUSB0</span>
<span class="go">GXL:BL1:9ac50e:bb16dc;FEAT:ADFC318C:0;POC:1;RCY:0;SPI:0;0.0;CHK:0;</span>
<span class="go">TE: 36574</span>

<span class="go">BL2 Built : 15:21:18, Aug 28 2019. gxl g1bf2b53 - luan.yuan@droid15-sz</span>

<span class="go">set vcck to 1120 mv</span>
<span class="go">set vddee to 1000 mv</span>
<span class="go">Board ID = 4</span>
<span class="go">CPU clk: 1200MHz</span>
<span class="go">[…]</span>
</code></pre></div>

<h1 id="lassemblage-nix">L’assemblage Nix</h1>
<p>Pour coller tout ensemble, j’utilise <em>Nix</em> avec un <a href="https://github.com/vincentbernat/offline-pki/blob/main/flake.nix">Flake</a> fournissant :</p>
<ul>
<li>un paquet pour l’application <code>offline-pki</code>, avec la complétion pour le shell ;</li>
<li>un shell de développement, incluant une version éditable de l’application <code>offline-pki</code> ;</li>
<li>un module NixOS pour configurer la <abbr title="Public Key Infrastructure">PKI</abbr>, réinitialisant le système à chaque démarrage ;</li>
<li>une image QEMU pour les tests ;</li>
<li>une image de carte SD à utiliser sur le <em>Sweet Potato</em> ou une autre carte ARM64.</li>
</ul>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Exécuter localement l'application</span>
nix<span class="w"> </span>run<span class="w"> </span>github:vincentbernat/offline-pki<span class="w"> </span>--<span class="w"> </span>--help
<span class="c1"># Exécuter l'application dans une machine virtuelle QEMU</span>
nix<span class="w"> </span>run<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>qemu
<span class="c1"># Construire l'image d'une carte SD pour Sweet Potato ou un Raspberry Pi</span>
nix<span class="w"> </span>build<span class="w"> </span>--system<span class="w"> </span>aarch64-linux<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>sdcard.potato
nix<span class="w"> </span>build<span class="w"> </span>--system<span class="w"> </span>aarch64-linux<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>sdcard.generic
<span class="c1"># Obtenir un shell de développement avec l'application</span>
nix<span class="w"> </span>develop<span class="w"> </span>github:vincentbernat/offline-pki
</code></pre></div>

<div class="footnote">
<hr/>
<ol>
<li id="fn:root">
<p>La clé pour l’autorité de certification racine n’est pas générée par la
YubiKey. L’utilisation d’un ordinateur isolé est d’autant plus importante.
Mettez-le dans un coffre-fort avec les YubiKeys quand vous avez terminé ! <a class="footnote-backref" href="#fnref:root" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Retour d'expérience sur Cisco pyATS et Genie Parser</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2021-pyats-genie-parser" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2021-pyats-genie-parser#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-09-12T14:31:17Z</updated>
    <id>http://www.luffy.cx/fr/blog/2021-pyats-genie-parser.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://developer.cisco.com/docs/pyats/">Cisco pyATS</a> est un outil pour l’automatisation et le test du
réseau. Il comprend, entre autres, un ensemble d’analyseurs et de
modèles multi-vendeurs open-source, <a href="https://github.com/CiscoTestAutomation/genieparser">Genie Parser</a>. Il dispose de
2700 analyseurs pour diverses commandes sur de nombreux systèmes. Sur
le papier, cela semble être un excellent outil !</p>
<div class="language-python-console-session codehilite"><pre><span/><code><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span><span class="w"> </span><span class="nn">genie.conf.base</span><span class="w"> </span><span class="kn">import</span> <span class="n">Device</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">device</span> <span class="o">=</span> <span class="n">Device</span><span class="p">(</span><span class="s2">"router"</span><span class="p">,</span> <span class="n">os</span><span class="o">=</span><span class="s2">"iosxr"</span><span class="p">)</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># Hack pour analyser le résultat d'une commande sans se connecter à l'équipement</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">device</span><span class="o">.</span><span class="n">custom</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s2">"abstraction"</span><span class="p">,</span> <span class="p">{})[</span><span class="s2">"order"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"os"</span><span class="p">,</span> <span class="s2">"platform"</span><span class="p">]</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">cmd</span> <span class="o">=</span> <span class="s2">"show route ipv4 unicast"</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">output</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="gp">... </span><span class="s2">Tue Oct 29 21:29:10.924 UTC</span>
<span class="gp">...</span>
<span class="gp">... </span><span class="s2">O    10.13.110.0/24 [110/2] via 10.12.110.1, 5d23h, GigabitEthernet0/0/0/0.110</span>
<span class="gp">... </span><span class="s2">"""</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">device</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">output</span><span class="o">=</span><span class="n">output</span><span class="p">)</span>
<span class="go">{'vrf': {'default': {'address_family': {'ipv4': {'routes': {'10.13.110.0/24': {'route': '10.13.110.0/24',</span>
<span class="go">       'active': True,</span>
<span class="go">       'route_preference': 110,</span>
<span class="go">       'metric': 2,</span>
<span class="go">       'source_protocol': 'ospf',</span>
<span class="go">       'source_protocol_codes': 'O',</span>
<span class="go">       'next_hop': {'next_hop_list': {1: {'index': 1,</span>
<span class="go">          'next_hop': '10.12.110.1',</span>
<span class="go">          'outgoing_interface': 'GigabitEthernet0/0/0/0.110',</span>
<span class="go">          'updated': '5d23h'}}}}}}}}}}</span>
</code></pre></div>

<p>Première déception : <em>pyATS</em> est sous licence propriétaire et les
sources ne sont pas disponibles. C’est assez ennuyeux si vous
rencontrez des problèmes en dehors de <em>Genie Parser</em>. Par exemple,
bien que <em>pyATS</em> utilise la commande <code>ssh</code>, il ne peut pas exploiter
mon fichier <code>ssh_config</code> : <em>pyATS</em> <a href="https://github.com/CiscoTestAutomation/pyats/issues/125" title="CiscoTestAutomation/pyats #125: Let ssh resolve hostname to leverage user's configuration">résout les noms d’hôtes avant de
les fournir à <code>ssh</code></a>. Il n’est <a href="https://github.com/CiscoTestAutomation/pyats#is-pyats-open-source">pas prévu de publier <em>pyATS</em>
sous licence libre</a>. 🤨</p>
<p>Ensuite, <em>Genie Parser</em> a deux problèmes :</p>
<ol>
<li>Les modèles de données utilisés dépendent du vendeur et du système
   d’exploitation, <del>alors que la <a href="https://developer.cisco.com/docs/genie-docs/">documentation</a> indique le
   contraire</del>. Par exemple, le modèle de données utilisé pour les
   interfaces IPv4 est différent entre <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/nxos/show_interface.py#L1144-L1226">NX-OS</a> et <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/iosxr/show_interface.py#L1184-L1219">IOS XR</a>.</li>
<li>Les analyseurs s’appuient sur des expressions régulières ligne par
   ligne pour extraire les données et sur une logique implémentée en
   Python. Tout cela est fragile et peut se briser silencieusement.</li>
</ol>
<p>Pour illustrer le deuxième point, supposons que la sortie de la
commande <code>show ipv4 vrf all interface</code> soit :</p>
<div class="language-text-only codehilite"><pre><span/><code>Loopback10 is Up, ipv4 protocol is Up
  Vrf is default (vrfid 0x60000000)
  Internet protocol processing disabled
<span class="hll">Loopback30 is Up, ipv4 protocol is Down [VRF in FWD reference]
</span>  Vrf is ran (vrfid 0x0)
  Internet address is 203.0.113.17/32
  MTU is 1500 (1500 is available to IP)
  Helper address is not set
  Directed broadcast forwarding is disabled
  Outgoing access list is not set
  Inbound  common access list is not set, access list is not set
  Proxy ARP is disabled
  ICMP redirects are never sent
  ICMP unreachables are always sent
  ICMP mask replies are never sent
  Table Id is 0x0
</code></pre></div>

<p>Parce que <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/iosxr/show_interface.py#L1246-L1248">l’expression régulière pour analyser un nom
d’interface</a> ne
s’attend pas aux données supplémentaires après l’état de l’interface,
<em>Genie Parser</em> ignore la ligne qui commence la définition de
<code>Loopback30</code> et analyse ainsi la sortie<sup id="fnref:model"><a class="footnote-ref" href="#fn:model">1</a></sup> :</p>
<div class="language-json codehilite"><pre><span/><code><span class="p">{</span>
<span class="w">  </span><span class="nt">"Loopback10"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nt">"int_status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"up"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"oper_status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"up"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"vrf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ran"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"vrf_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x0"</span><span class="p">,</span>
<span class="w">    </span><span class="nt">"ipv4"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">      </span><span class="nt">"203.0.113.17/32"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nt">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"203.0.113.17"</span><span class="p">,</span>
<span class="w">        </span><span class="nt">"prefix_length"</span><span class="p">:</span><span class="w"> </span><span class="s2">"32"</span>
<span class="w">      </span><span class="p">},</span>
<span class="w">      </span><span class="nt">"mtu"</span><span class="p">:</span><span class="w"> </span><span class="mi">1500</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"mtu_available"</span><span class="p">:</span><span class="w"> </span><span class="mi">1500</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"broadcast_forwarding"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"proxy_arp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disabled"</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"icmp_redirects"</span><span class="p">:</span><span class="w"> </span><span class="s2">"never sent"</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"icmp_unreachables"</span><span class="p">:</span><span class="w"> </span><span class="s2">"always sent"</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"icmp_replies"</span><span class="p">:</span><span class="w"> </span><span class="s2">"never sent"</span><span class="p">,</span>
<span class="w">      </span><span class="nt">"table_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x0"</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>Bien que ce défaut soit simple à corriger, il s’agit d’une bataille
difficile. Toute variation existante ou future dans la sortie d’une
commande peut déclencher un bogue similaire non détecté, malgré la
bonne couverture de tests. J’ai signalé et corrigé plusieurs autres
erreurs d’analyse « silencieuses » : <a href="https://github.com/CiscoTestAutomation/genieparser/pull/516" title="CiscoTestAutomation/genieparser #516: IOSXR/NXOS: fix show static routing parsers to correctly handle IPv6">#516</a>, <a href="https://github.com/CiscoTestAutomation/genieparser/pull/529" title="CiscoTestAutomation/genieparser #529: IOSXR: fix show route ipv4 parser for hops without interface">#529</a> et <a href="https://github.com/CiscoTestAutomation/genieparser/pull/530" title="CiscoTestAutomation/genieparser #530: IOSXR: fix AS parsing for dynamic neighbors">#530</a>.
Une alternative plus robuste aurait été d’utiliser <a href="https://github.com/google/textfsm" title="Python module for parsing semi-structured text into python tables">TextFSM</a> et de
produire un avertissement lorsque certaines sorties ne sont pas
reconnues, comme le fait <a href="https://batfish.org/" title="An open source network configuration analysis tool">Batfish</a>, un outil d’analyse de
configurations.</p>
<p>À l’avenir, nous pourrons nous appuyer sur <a href="https://www.rfc-editor.org/rfc/rfc6020" title="RFC 6020: YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)">YANG</a> pour l’extraction
de données, mais il n’est actuellement pas largement pris en charge
par le parc actuel. <abbr title="Simple Network Management Protocol">SNMP</abbr> reste une possibilité valable, mais de
nombreuses informations ne sont pas accessibles via ce protocole. En
attendant, je vous conseille de n’utiliser <em>Genie Parser</em> qu’avec une
grande prudence.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:model">
<p>À titre d’exercice, le lecteur est invité à écrire le code
pour extraire l’IPv4 de cette structure. <a class="footnote-backref" href="#fnref:model" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Passage au gestionnaire de fenêtres i3</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2021-i3-window-manager" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2021-i3-window-manager#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-09-06T05:45:47Z</updated>
    <id>http://www.luffy.cx/fr/blog/2021-i3-window-manager.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>J’utilise <a href="https://awesomewm.org/">awesome</a> depuis 10 ans. Il s’agit
d’un <a href="https://fr.wikipedia.org/wiki/Gestionnaire_de_fen%C3%AAtres_par_pavage">gestionnaire de fenêtres à tuiles</a>,
configurable et extensible avec le langage <a href="https://www.lua.org/">Lua</a>. L’utilisation d’un
langage de programmation polyvalent pour configurer chaque aspect est
une arme à double tranchant. Par paresse et à cause de la difficulté
apparente d’adapter <a href="https://github.com/vincentbernat/awesome-configuration/">ma configuration</a> d’environ
3000 lignes aux nouvelles versions, je suis resté coincé avec la
version 3.4, dont la dernière version date de 2013.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1920px"><span class="lf-media-inner" style="padding-bottom: 112.500%"><a href="https://media-bernat-ch.b-cdn.net/images/i3/desktop@2x.258eacf7793d57.jpg"><img alt="i3 sur deux écrans" src="https://media-bernat-ch.b-cdn.net/images/i3/desktop@1x.dcd1e55ef3b240.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/i3/desktop@1x.dcd1e55ef3b240.jpg 2413w,https://media-bernat-ch.b-cdn.net/images/i3/desktop@2x.258eacf7793d57.jpg 3840w" sizes="auto, (max-width: 972px) 100vw, 900px" width="1920" height="2160" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAJAQMAAAA1nf9tAAAAA1BMVEXp5NpdXdvXAAAAC0lEQVR42mNgQAcAABIAAeRVjecAAAAASUVORK5CYII=)"/></a></span></div><figcaption>Configuration en double écran faisant tourner i3, Emacs, quelques terminaux, dont une console à la Quake, Firefox, Polybar comme barre de statut et Dunst pour afficher les notifications.</figcaption></figure>
<p>Le gestionnaire de fenêtres n’est qu’une partie d’un environnement de
bureau. Il existe plusieurs options pour les autres composants. Je les
présente également dans ce billet.</p>
<div class="toc">
<ul>
<li><a href="#i3-le-gestionnaire-de-fenetres">i3 : le gestionnaire de fenêtres</a></li>
<li><a href="#le-compagnon-i3-le-chainon-manquant">le compagnon i3 : le chaînon manquant</a></li>
<li><a href="#dunst-le-demon-de-notification">dunst : le démon de notification</a></li>
<li><a href="#polybar-la-barre-de-statut">polybar : la barre de statut</a></li>
<li><a href="#picom-le-compositeur">picom : le compositeur</a></li>
<li><a href="#systemd-le-gestionnaire-de-services">systemd : le gestionnaire de services</a></li>
<li><a href="#rofi-le-lanceur-dapplications">rofi : le lanceur d’applications</a></li>
<li><a href="#xss-lock-et-i3lock-le-verrouillage-de-la-session">xss-lock et i3lock : le verrouillage de la session</a></li>
<li><a href="#les-autres-composants">Les autres composants</a></li>
</ul>
</div>
<h1 id="i3-le-gestionnaire-de-fenetres">i3 : le gestionnaire de fenêtres</h1>
<p><em>i3</em> est un gestionnaire de fenêtres à tuiles (« <em>tiling window
manager</em> ») minimaliste. Sa <a href="https://i3wm.org/docs/userguide.html" title="i3 User's Guide">documentation</a> peut être lue du début à
la fin en moins d’une heure. <em>i3</em> organise les fenêtres dans un
<a href="https://i3wm.org/docs/userguide.html#_tree" title="i3 User's Guide: Tree">arbre</a>. Chaque nœud intermédiaire contient une ou plusieurs
fenêtres et possède une orientation et une disposition. Ces
informations permettent d’arbitrer la position des fenêtres. <em>i3</em>
propose trois dispositions : fractionnée (<em>split</em>), empilée
(<em>stacking</em>) et à onglets (<em>tabbed</em>). Elles sont présentées dans la
capture d’écran ci-dessous :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 600px"><span class="lf-media-inner" style="padding-bottom: 56.167%"><img alt="Exemple de dispositions" src="https://media-bernat-ch.b-cdn.net/images/i3/layouts@1x.125b5c9bf3876f.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/i3/layouts@1x.125b5c9bf3876f.jpg 754w,https://media-bernat-ch.b-cdn.net/images/i3/layouts@2x.ebd547ee7dc2aa.jpg 1200w" sizes="auto, (max-width: 600px) 100vw, 600px" width="600" height="337" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAJAQMAAAAB5D5xAAAAA1BMVEUAAACnej3aAAAAC0lEQVR42mNgwAkAABsAAVmYPeoAAAAASUVORK5CYII=)"/></span></div><figcaption>Démonstration des dispositions dans i3. Le conteneur principal est divisé horizontalement. Le premier enfant est divisé verticalement. Le deuxième utilise des onglets. Le dernier est empilé.</figcaption></figure>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 477px"><span class="lf-media-inner" style="padding-bottom: 93.711%"><img alt="Représentation en arbre de la capture d'écran&#10;précédente" src="https://media-bernat-ch.b-cdn.net/images/i3/layouts.574313ba154777.svg" width="477" height="447" class="lf-media"/></span></div><figcaption>Représentation en arbre de la capture d'écran précédente.</figcaption></figure>
<p>La plupart des autres gestionnaires de fenêtres à tuiles, y compris
<em>awesome</em>, utilisent des dispositions prédéfinies. Ils présentent
généralement une zone importante pour la fenêtre principale et une
seconde zone découpée entre les autres fenêtres. Ces dispositions
peuvent être légèrement modifiées, mais vous vous en tenez
généralement à une ou deux d’entre elles. Lorsqu’une nouvelle fenêtre
est ajoutée, le comportement est assez prévisible. De plus, vous
pouvez parcourir les différentes fenêtres sans trop réfléchir car
elles sont ordonnées.</p>
<p><em>i3</em> est plus flexible grâce à sa capacité à construire n’importe
quelle disposition à la volée, mais cela peut déconcerter car vous
devez visualiser l’arbre dans votre tête. Au début, il n’est pas rare
de se retrouver avec une arborescence complexe comportant de nombreux
conteneurs imbriqués superflus. De plus, vous devez naviguer dans les
fenêtres à l’aide de directions. Il faut un certain temps pour s’y
habituer.</p>
<p>J’ai configuré une disposition en deux parties pour Emacs et quelques
terminaux, mais la plupart des autres espaces de travail utilisent des
onglets. Je n’utilise pas la disposition par empilement. Vous pouvez
trouver de nombreux scripts essayant d’émuler d’autres gestionnaires
de fenêtres à tuiles, mais j’ai essayé de ne pas me laisser tenter dès
le début et de prendre le temps de m’habituer. i3 peut également
<a href="https://i3wm.org/docs/layout-saving.html" title="Layout saving in i3">sauvegarder et restaurer les dispositions</a>,
ce qui est une fonctionnalité assez puissante.</p>
<p><a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/config" title="i3 configuration file">Ma configuration</a> est proche de la configuration par défaut
avec moins de 200 lignes.</p>
<h1 id="le-compagnon-i3-le-chainon-manquant">le compagnon i3 : le chaînon manquant</h1>
<p>La philosophie de <em>i3</em> est de rester minimaliste et de laisser
l’utilisateur implémenter les fonctionnalités manquantes en utilisant
un protocole de communication (« <em><abbr title="Inter-process communication">IPC</abbr></em> ») :</p>
<blockquote>
<p>Ne pas ajouter de complexité supplémentaire lorsque cela peut être
évité. Nous sommes généralement satisfaits de l’ensemble des
fonctionnalités de i3 et nous nous concentrons plutôt sur la
correction des bogues et la maintenance de la stabilité. De
nouvelles fonctionnalités ne seront donc envisagées que si les
avantages l’emportent sur la complexité supplémentaire, et nous
encourageons les utilisateurs à mettre en œuvre des fonctionnalités
en utilisant le protocole <abbr title="Inter-process communication">IPC</abbr> chaque fois que cela est possible.</p>
<p>— <a href="https://i3wm.org/">Introduction au gestionnaire de fenêtres i3</a></p>
</blockquote>
<p>Bien que cela ne soit pas aussi versatile qu’un langage intégré, c’est
suffisant pour de nombreux cas. De plus, comme les fonctionnalités de
haut niveau sont souvent subjectives, les déléguer à de petits
morceaux de code faiblement couplés les rend plus faciles à maintenir.
Des bibliothèques existent à cet effet dans <a href="https://i3wm.org/docs/ipc.html#_see_also_existing_libraries" title="IPC interface (interprocess communication): existing libraries">plusieurs
langages</a>. Les utilisateurs ont publié de nombreux
scripts pour étendre <em>i3</em> : <a href="https://aduros.com/blog/hacking-i3-automatic-layout/" title="Hacking i3: Automatic Layout">mise en page automatique</a> et <a href="https://aduros.com/blog/hacking-i3-window-promoting/" title="Hacking i3: Window Promoting">promotion de fenêtres</a> pour imiter le
comportement d’autres gestionnaires de fenêtres, <a href="https://aduros.com/blog/hacking-i3-window-swallowing/" title="Hacking i3: Window Swallowing">remplacement de
fenêtres</a> pour placer une nouvelle application
au-dessus du terminal qui la lance et <a href="https://github.com/altdesktop/i3ipc-python/blob/master/examples/i3-cycle-focus.py">passage d’une fenêtre à
l’autre</a> avec <kbd>Alt</kbd>+<kbd>Tab</kbd>.</p>
<p>Au lieu de maintenir un script pour chaque fonctionnalité, j’ai tout
centralisé dans un seul processus Python,
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/i3-companion"><code>i3-companion</code></a>, utilisant <a href="https://docs.python.org/3/library/asyncio.html">asyncio</a> et la
bibliothèque <a href="https://i3ipc-python.readthedocs.io/en/latest/" title="i3ipc-python Documentation">i3ipc-python</a>. Chaque fonctionnalité est confinée dans
une fonction. Il implémente les composants suivants :</p>
<dl>
<dt>Rendre un espace de travail exclusif à une application</dt>
<dd>Lorsqu’un espace de travail contient Emacs ou Firefox, j’aimerais
  que les autres applications se placent dans un autre espace de
  travail, à l’exception du terminal qui est autorisé à « envahir »
  n’importe quel espace de travail. La fonction
  <code>workspace_exclusive()</code> surveille les nouvelles fenêtres et les
  déplace si nécessaire vers un espace de travail vide ou contenant la
  même application déjà en cours d’exécution.</dd>
<dt>Implémenter une console Quake</dt>
<dd>La fonction <code>quake_console()</code> implémente une console déroulante
  disponible depuis n’importe quel espace de travail. Elle peut être
  activée avec <kbd>Mod</kbd>+<kbd>`</kbd>. Elle utilise le mécanisme
  de <a href="https://i3wm.org/docs/userguide.html#_scratchpad" title="i3's User Guide: Scratchpad">scratchpad</a>.</dd>
<dt>Alterner entre les deux derniers espaces de travail sur un écran</dt>
<dd>Avec la commande <code>workspace back_and_forth</code>, on peut demander à <em>i3</em>
  de passer à l’espace de travail précédent. Cependant, cette
  fonctionnalité n’est pas limitée à l’écran actuel. Je préfère avoir
  un raccourci clavier pour passer à l’espace de travail sur l’écran
  d’à côté et un autre pour passer à l’espace de travail précédent sur
  le même écran. Ce comportement est mis en place dans la fonction
  <code>previous_workspace()</code> en conservant un historique par écran.</dd>
<dt>Créer un nouvel espace de travail vide ou déplacer une fenêtre vers un espace de travail vide</dt>
<dd>Pour créer un nouvel espace de travail vide ou y déplacer une
  fenêtre, vous devez trouver un emplacement non utilisé et invoquer
  le raccourci pour <code>workspace number 4</code> ou <code>move container to
  workspace number 4</code>. La fonction <code>new_workspace()</code> trouve un
  emplacement libre et l’utilise comme espace de travail cible.</dd>
<dt>Redémarrer certains services lors d’un changement de configuration des écrans</dt>
<dd>Lors de l’ajout ou de la suppression d’un écran, certaines actions
  doivent être exécutées : rafraîchir le fond d’écran, redémarrer
  certains composants incapables d’adapter leur configuration par
  eux-mêmes, etc. <em>i3</em> déclenche un événement à cet effet. La fonction
  <code>output_update()</code> fusionne plusieurs événements consécutifs et
  vérifie s’il y a un réel changement avec la bibliothèque de bas
  niveau <a href="https://github.com/tych0/xcffib" title="drop-in replacement for xpyb based on cffi">xcffib</a>.</dd>
</dl>
<p>Je détaillerai les autres fonctionnalités au fur et à mesure de ce
billet. Concernant la partie technique, chaque fonction est décorée
avec les événements auxquels elle doit réagir :</p>
<div class="language-python codehilite"><pre><span/><code><span class="nd">@on</span><span class="p">(</span><span class="n">CommandEvent</span><span class="p">(</span><span class="s2">"previous-workspace"</span><span class="p">),</span> <span class="n">I3Event</span><span class="o">.</span><span class="n">WORKSPACE_FOCUS</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">previous_workspace</span><span class="p">(</span><span class="n">i3</span><span class="p">,</span> <span class="n">event</span><span class="p">):</span>
<span class="w">    </span><span class="sd">"""Go to previous workspace on the same output."""</span>
</code></pre></div>

<p>La classe d’évènements <code>CommandEvent()</code> correspond à ma façon
d’envoyer une commande au compagnon, en utilisant soit <code>i3-msg -t
send_tick</code> ou en liant un raccourci à une commande <code>nop</code>. Cette
dernière est utilisée pour éviter d’invoquer un shell et un processus
<code>i3-msg</code> juste pour envoyer un message. Le compagnon écoute les
événements liés aux raccourcis clavier et vérifie si c’est une
commande <code>nop</code>.</p>
<div class="language-text-only codehilite"><pre><span/><code>bindsym $mod+Tab nop "previous-workspace"
</code></pre></div>

<p>Il existe d’autres décorateurs pour éviter la duplication du code :
<code>@debounce()</code> pour fusionner plusieurs appels consécutifs, <code>@static()</code>
pour définir une variable statique, et <code>@retry()</code> pour relancer une
fonction en cas d’échec. Le <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/i3-companion">script entier</a> fait un peu
plus de 1000 lignes. Je pense que cela vaut la peine de le lire car je
suis assez satisfait du résultat. 🦚</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (07.2022)</p>
<p><a href="https://github.com/kriansa">Daniel Pereira</a> a écrit <a href="https://github.com/kriansa/wmcompanion">wmcompanion</a>, un moniteur d’événements pour
les utilisateurs de gestionnaires de fenêtres minimaux, inspiré par
<em>i3-companion</em>. C’est un moteur générique pour construire votre propre
compagnon. Il n’a pas de support intégré pour les événements i3, mais sa
conception modulaire permet à un utilisateur motivé de l’ajouter avec
quelques lignes de code.</p>
</div>
<h1 id="dunst-le-demon-de-notification">dunst : le démon de notification</h1>
<p>Contrairement à <em>awesome</em>, <em>i3</em> ne dispose pas d’un système de
notification intégré. <a href="https://dunst-project.org/">Dunst</a> est un démon de notification léger.
J’utilise une version modifiée avec le <a href="https://github.com/dunst-project/dunst/pull/890" title="PR #890: X11: enable dpi scaling">support HiDPI pour X11</a> et la <a href="https://github.com/dunst-project/dunst/pull/864" title="PR #864: Recursive icon lookup">recherche récursive des icônes</a>. Le <em>compagnon i3</em> a une fonction auxiliaire, <code>notify()</code>, pour
envoyer des notifications en utilisant DBus. <code>container_info()</code> et
<code>workspace_info()</code> l’utilisent pour afficher des informations sur le
conteneur ou l’arbre d’un espace de travail.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 416px"><span class="lf-media-inner" style="padding-bottom: 39.423%"><img alt="Notification montrant l'arbre i3 d'un espace de&#10;travail" src="https://media-bernat-ch.b-cdn.net/images/i3/workspace-info@1x.e0c9ed7b7fb4ba.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/i3/workspace-info@1x.e0c9ed7b7fb4ba.jpg 523w,https://media-bernat-ch.b-cdn.net/images/i3/workspace-info@2x.04824698986731.jpg 832w" sizes="auto, (max-width: 416px) 100vw, 416px" width="416" height="164" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA0AAAAFJAQMAAACIPd/gAAAAA1BMVEUhIyL1PCeIAAAAN0lEQVR42u3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcGGG8QABN330MgAAAABJRU5ErkJggg==)"/></span></div><figcaption>Notification montrant l'arbre i3 d'un espace de travail</figcaption></figure>
<h1 id="polybar-la-barre-de-statut">polybar : la barre de statut</h1>
<p><em>i3</em> inclut <a href="https://i3wm.org/i3bar/">i3bar</a>, une barre de statut polyvalente, mais j’ai opté
pour <a href="https://github.com/polybar/polybar" title="Fast and easy-to-use bar">Polybar</a>. Un <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/polybar">script</a> exécute une instance
pour chaque écran.</p>
<p>Le premier module est le support intégré pour les espaces de travail
<em>i3</em>. Pour ne pas avoir à se rappeler quelle application est en cours
d’exécution dans un espace de travail, le <em>compagnon i3</em> renomme les
espaces de travail pour inclure une icône pour chaque application.
Cette opération est effectuée par la fonction <code>workspace_rename()</code>.
Les icônes proviennent du projet <a href="https://fontawesome.com/">Font Awesome</a>. Je maintiens
manuellement la correspondance entre les applications et les icônes.
C’est un peu lourd, mais c’est très joli.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 265px"><span class="lf-media-inner" style="padding-bottom: 6.792%"><img alt="Espaces de travail dans Polybar" src="https://media-bernat-ch.b-cdn.net/images/i3/workspaces@1x.34519898944a9a.png" srcset="https://media-bernat-ch.b-cdn.net/images/i3/workspaces@1x.34519898944a9a.png 333w,https://media-bernat-ch.b-cdn.net/images/i3/workspaces@2x.103374fc7d7e7d.png 530w" sizes="auto, (max-width: 265px) 100vw, 265px" width="265" height="18" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQkAAAASAQMAAABywLnLAAAAA1BMVEUAAACnej3aAAAAD0lEQVR42mNgGAWjgN4AAAJ2AAEQn0YhAAAAAElFTkSuQmCC)"/></span></div><figcaption>Les espaces de travail d'i3 dans Polybar</figcaption></figure>
<p>Pour le CPU, la mémoire, la luminosité, la batterie, le disque et le
volume sonore, je m’appuie sur les modules intégrés. Le
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/polybar">script</a> lançant <em>Polybar</em> génère la liste des
systèmes de fichiers à surveiller et ils ne sont affichés que lorsque
l’espace disponible est faible. L’icône de la batterie devient rouge
et clignote lentement lorsqu’elle est à court d’énergie. Consultez ma
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/polybar.conf">configuration de Polybar</a> pour plus de
détails.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 553px"><span class="lf-media-inner" style="padding-bottom: 3.255%"><img alt="Divers modules pour Polybar" src="https://media-bernat-ch.b-cdn.net/images/i3/polybar@1x.2743d17eca2b2d.png" srcset="https://media-bernat-ch.b-cdn.net/images/i3/polybar@1x.2743d17eca2b2d.png 695w,https://media-bernat-ch.b-cdn.net/images/i3/polybar@2x.b99f0d8939b81f.png 1106w" sizes="auto, (max-width: 553px) 100vw, 553px" width="553" height="18" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAikAAAASAQMAAACkf9waAAAAA1BMVEVbZm3HNy1PAAAAEklEQVR42mNgGAWjYBSMgpEIAAT+AAHJAUHeAAAAAElFTkSuQmCC)"/></span></div><figcaption>Polybar affiche diverses informations : utilisation du CPU, utilisation de la mémoire, luminosité de l'écran, état de la batterie, état du bluetooth (avec un casque connecté), état du réseau (connecté à un réseau sans fil et à un VPN), état des notifications et volume sonore.</figcaption></figure>
<p>Pour les statuts du Bluetooth, du réseau et des notifications,
j’utilise le <a href="https://github.com/polybar/polybar/wiki/Module:-ipc">module <code>ipc</code></a> de <em>Polybar</em> : la prochaine
version permet de <a href="https://github.com/polybar/polybar/pull/2463" title="PR #2463: feat(ipc): allow receiving arbitrary text on IPC socket">recevoir un texte arbitraire via le canal
<abbr title="Inter-process communication">IPC</abbr></a>. Le module est défini avec une
seule commande à exécuter au démarrage pour restaurer le dernier
statut.</p>
<div class="language-ini codehilite"><pre><span/><code><span class="k">[module/network]</span>
<span class="na">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">custom/ipc</span>
<span class="na">hook-0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">cat $XDG_RUNTIME_DIR/i3/network.txt 2&gt; /dev/null</span>
<span class="na">initial</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">1</span>
</code></pre></div>

<p>Il peut être mis à jour avec la commande <code>polybar-msg action
"#network.send.XXXX"</code>. Dans le <em>compagnon i3</em>, le décorateur
<code>@polybar()</code> prend la chaîne retournée par une fonction et pousse la
mise à jour à travers la chaussette <abbr title="Inter-process communication">IPC</abbr>.</p>
<p>Le <em>compagnon i3</em> réagit aux signaux DBus pour mettre à jour les
icônes Bluetooth et réseau. Le décorateur <code>@on()</code> accepte un objet
<code>DBusSignal()</code>:</p>
<div class="language-python codehilite"><pre><span/><code><span class="nd">@on</span><span class="p">(</span>
    <span class="n">StartEvent</span><span class="p">,</span>
    <span class="n">DBusSignal</span><span class="p">(</span>
        <span class="n">path</span><span class="o">=</span><span class="s2">"/org/bluez"</span><span class="p">,</span>
        <span class="n">interface</span><span class="o">=</span><span class="s2">"org.freedesktop.DBus.Properties"</span><span class="p">,</span>
        <span class="n">member</span><span class="o">=</span><span class="s2">"PropertiesChanged"</span><span class="p">,</span>
        <span class="n">signature</span><span class="o">=</span><span class="s2">"sa</span><span class="si">{sv}</span><span class="s2">as"</span><span class="p">,</span>
        <span class="n">onlyif</span><span class="o">=</span><span class="k">lambda</span> <span class="n">args</span><span class="p">:</span> <span class="p">(</span>
            <span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"org.bluez.Device1"</span>
            <span class="ow">and</span> <span class="s2">"Connected"</span> <span class="ow">in</span> <span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
            <span class="ow">or</span> <span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"org.bluez.Adapter1"</span>
            <span class="ow">and</span> <span class="s2">"Powered"</span> <span class="ow">in</span> <span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
        <span class="p">),</span>
    <span class="p">),</span>
<span class="p">)</span>
<span class="nd">@retry</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="nd">@debounce</span><span class="p">(</span><span class="mf">0.2</span><span class="p">)</span>
<span class="nd">@polybar</span><span class="p">(</span><span class="s2">"bluetooth"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">bluetooth_status</span><span class="p">(</span><span class="n">i3</span><span class="p">,</span> <span class="n">event</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span>
<span class="w">    </span><span class="sd">"""Update bluetooth status for Polybar."""</span>
</code></pre></div>

<p>Le milieu de la barre est occupé par la date et la météo. Cette
dernière utilise également le mécanisme <abbr title="Inter-process communication">IPC</abbr>, mais la source est un
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/weather">script Python</a> déclenché régulièrement.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 367px"><span class="lf-media-inner" style="padding-bottom: 4.905%"><img alt="Date et météo dans Polybar" src="https://media-bernat-ch.b-cdn.net/images/i3/weather@1x.2027ad14f4241a.png" srcset="https://media-bernat-ch.b-cdn.net/images/i3/weather@1x.2027ad14f4241a.png 461w,https://media-bernat-ch.b-cdn.net/images/i3/weather@2x.b08a3dc17bd928.png 734w" sizes="auto, (max-width: 367px) 100vw, 367px" width="367" height="18" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPUAAAAMAQMAAACA4mn3AAAAA1BMVEUhOTy/HuAwAAAADUlEQVR42mNgGAUDCQABgAABa8sh/AAAAABJRU5ErkJggg==)"/></span></div><figcaption>Date actuelle et prévisions météorologiques pour la journée dans Polybar. Les données sont récupérées avec l'API OpenWeather.</figcaption></figure>
<p>Je n’utilise pas la <em>zone de notification</em> intégrée à <em>Polybar</em>. Les
icônes sont généralement hideuses et se comportent toutes
différemment. Il y a quelques années, <a href="https://blogs.gnome.org/aday/2017/08/31/status-icons-and-gnome/" title="Status Icons and GNOME">GNOME a supprimé la zone de
notification</a>. La plupart des
problèmes sont résolus par le protocole <a href="https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/">Status Notifier Item</a> basé sur DBus. Toutefois, <a href="https://github.com/polybar/polybar/issues/2010" title="No support for SystemNotifier tray icons (electron apps)"><em>Polybar</em> ne prend
pas en charge celui-ci</a>.
L’implémentation des icônes Bluetooth et réseau, y compris l’affichage
des notifications en cas de changement, prend environ 200 lignes dans
le <em>compagnon i3</em>. J’ai pu apprendre un peu comment fonctionne DBus et
j’obtiens exactement les informations que je veux.</p>
<h1 id="picom-le-compositeur">picom : le compositeur</h1>
<p>J’aime avoir des terminaux légèrement transparents et réduire
l’opacité des fenêtres inactives. Ces tâches nécessitent un
compositeur<sup id="fnref:tearfree"><a class="footnote-ref" href="#fn:tearfree">1</a></sup>. <a href="https://github.com/yshui/picom">picom</a> est un compositeur léger. Il
fonctionne bien dans mon cas, mais il peut nécessiter quelques
ajustements en fonction de votre carte graphique<sup id="fnref:intel"><a class="footnote-ref" href="#fn:intel">2</a></sup>.
Contrairement à <em>awesome</em>, <em>i3</em> ne gère pas la transparence, donc le
compositeur doit décider l’opacité de chaque fenêtre. Consultez <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/picom.conf">ma
configuration</a> pour plus de détails.</p>
<h1 id="systemd-le-gestionnaire-de-services">systemd : le gestionnaire de services</h1>
<p>J’utilise <a href="https://systemd.io">systemd</a> pour démarrer <em>i3</em> et les différents services
qui l’entourent. Mon <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/xsession">script <code>xsession</code></a> ne définit
que quelques variables d’environnement et laisse <em>systemd</em> s’occuper
de tout le reste. Jetez un coup d’œil à <a href="https://goral.net.pl/post/systemd-x-sessions/" title="Using systemd for X sessions">cet article de Michał
Góral</a> pour en connaître les raisons.
Notamment, chaque composant peut être facilement redémarré et leurs
sorties ne sont pas mélangées dans le fichier
<code>~/.xsession-errors</code><sup id="fnref:limitation"><a class="footnote-ref" href="#fn:limitation">3</a></sup>.</p>
<p>J’utilise un démarrage en deux étapes : <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/systemd/i3.service"><code>i3.service</code></a>
dépend de <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/systemd/xsession.target"><code>xsession.target</code></a> pour démarrer les
services avant <em>i3</em> :</p>
<div class="language-systemd codehilite"><pre><span/><code><span class="k">[Unit]</span>
<span class="na">Description</span><span class="o">=</span><span class="s">X session</span>
<span class="na">BindsTo</span><span class="o">=</span><span class="s">graphical-session.target</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">autorandr.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">dunst.socket</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">inputplug.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">picom.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">pulseaudio.socket</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">policykit-agent.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">redshift.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">spotify-clean.timer</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">ssh-agent.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">weather.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">weather.timer</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">xiccd.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">xsettingsd.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">xss-lock.service</span>
</code></pre></div>

<p>Ensuite, <em>i3</em> active la seconde étape via
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/systemd/i3-session.target"><code>i3-session.target</code></a>:</p>
<div class="language-systemd codehilite"><pre><span/><code><span class="k">[Unit]</span>
<span class="na">Description</span><span class="o">=</span><span class="s">i3 session</span>
<span class="na">BindsTo</span><span class="o">=</span><span class="s">graphical-session.target</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">wallpaper.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">wallpaper.timer</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">polybar.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">i3-companion.service</span>
<span class="na">Wants</span><span class="o">=</span><span class="s">misc-x.service</span>
</code></pre></div>

<p>Jetez un œil à <a href="https://github.com/vincentbernat/i3wm-configuration/tree/master/dotfiles/systemd">mes fichiers de configuration</a> pour plus de détails.</p>
<h1 id="rofi-le-lanceur-dapplications">rofi : le lanceur d’applications</h1>
<p><a href="https://github.com/davatorium/rofi/" title="Rofi: A window switcher, application launcher and dmenu replacement">Rofi</a> est un lanceur d’applications. Son apparence peut être
modifiée en utilisant un langage semblable à <abbr title="Cascading Style Sheets">CSS</abbr> et il est livré avec
plusieurs thèmes. Regardez ma <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/rofi.conf">configuration</a> pour le
mien.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 500px"><span class="lf-media-inner" style="padding-bottom: 69.200%"><img alt="Rofi en lanceur d'applications" src="https://media-bernat-ch.b-cdn.net/images/i3/rofi-drun@1x.5836de118fb826.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/i3/rofi-drun@1x.5836de118fb826.jpg 629w,https://media-bernat-ch.b-cdn.net/images/i3/rofi-drun@2x.439a4a0ae772a8.jpg 1000w" sizes="auto, (max-width: 500px) 100vw, 500px" width="500" height="346" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAK1AQMAAABhClY6AAAAA1BMVEUmKjNLAaq2AAAAa0lEQVR42u3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBtVSUAAYgcQ2oAAAAASUVORK5CYII=)"/></span></div><figcaption>Rofi en lanceur d'applications</figcaption></figure>
<p>Il peut également faire office de menu. J’ai écrit un script pour
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/rofi-mediaplayer">contrôler un lecteur multimédia</a> et un autre pour
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/rofi-wifi">sélectionner le réseau sans fil</a>. C’est une application
plutôt flexible.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 500px"><span class="lf-media-inner" style="padding-bottom: 77.200%"><img alt="Rofi pour sélectionner le réseau sans&#10;fil" src="https://media-bernat-ch.b-cdn.net/images/i3/rofi-wifi@1x.f88a14a3e0b433.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/i3/rofi-wifi@1x.f88a14a3e0b433.jpg 629w,https://media-bernat-ch.b-cdn.net/images/i3/rofi-wifi@2x.1b72d07eb38a42.jpg 1000w" sizes="auto, (max-width: 500px) 100vw, 500px" width="500" height="386" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAMFAQMAAADdt077AAAAA1BMVEUmKjNLAaq2AAAAdUlEQVR42u3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4G3yFAAEkJr2ZAAAAAElFTkSuQmCC)"/></span></div><figcaption>Rofi pour sélectionner le réseau sans fil</figcaption></figure>
<h1 id="xss-lock-et-i3lock-le-verrouillage-de-la-session">xss-lock et i3lock : le verrouillage de la session</h1>
<p><a href="https://i3wm.org/i3lock/">i3lock</a> permet de verrouiller la session. <a href="https://bitbucket.org/raymonad/xss-lock/src/master/">xss-lock</a> l’invoque de
manière fiable en cas d’inactivité ou avant une mise en veille. Pour
l’inactivité, il utilise les <a href="https://manpages.debian.org/Xss.3.html" title="Page de manuel pour Xss(3)">événements XScreenSaver</a>. Le délai est configuré via la commande <code>xset s</code>. Le
verrouillage peut être instantané avec <code>xset s activate</code>. Les
applications X11 savent comment le désactiver. J’ai également
développé une petite <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xss-dimmer">application de fondu au noir</a> qui est exécutée 20 secondes avant le verrouillage pour
me donner une chance de bouger la souris si je ne suis pas
absent<sup id="fnref:dimmer"><a class="footnote-ref" href="#fn:dimmer">4</a></sup>. Jetez un coup d’oeil à mon <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xss-lock">script de
configuration</a>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 480px"><span class="lf-media-inner" style="padding-bottom: 56.250%"><video src="https://media-bernat-ch.b-cdn.net/images/i3/locker.65cd509409bb91.mp4" width="480" height="270" muted="" loop="" autoplay="" playsinline="" controls="" class="lf-media lf-opaque"/></span></div><figcaption>Démonstration accélérée de xss-lock, xss-dimmer et i3lock.</figcaption></figure>
<div class="admonition">
<p class="admonition-title">Mise à jour (12.2021)</p>
<p>Je suis passé à <a href="https://github.com/google/xsecurelock" title="X11 screen lock utility with security in mind">XSecureLock</a> en utilisant un <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xsecurelock-saver">script
Python</a> pour personnaliser l’apparence de l’écran de
veille en y incluant une horloge et la météo. Voir l’article « <a href="/fr/blog/2021-xsecurelock">Économiseur
d’écran personnalisé avec XSecureLock</a> ».</p>
</div>
<h1 id="les-autres-composants">Les autres composants</h1>
<ul>
<li>
<p><a href="https://github.com/phillipberndt/autorandr/" title="Auto-detect the connected display hardware and load the appropriate X11 setup using xrandr">autorandr</a> est un outil qui détecte les écrans connectés, les
  compare à un ensemble de profils et les configure avec <code>xrandr</code>.</p>
</li>
<li>
<p><a href="https://github.com/andrewshadura/inputplug" title="XInput event monitor daemon">inputplug</a> exécute un script pour chaque nouvelle souris et
  clavier branchés. Regardez <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/input-event">ma configuration</a>
  pour avoir une idée des possibilités.</p>
</li>
<li>
<p><a href="https://github.com/derat/xsettingsd" title="Provides settings to X11 applications via the XSETTINGS specification">xsettingsd</a> fournit des paramètres aux applications X11, un peu
  comme <a href="https://manpages.debian.org/xrdb.1.html" title="Page de manuel pour xrdb(1)">xrdb</a> mais il notifie les applications des changements.
  L’utilisation principale est de configurer les paramètres Gtk et
  DPI. Lisez mon article sur le <a href="https://github.com/dunst-project/dunst/pull/890" title="PR #890: X11: enable dpi scaling">support HiDPI sur Linux avec
  X11</a> pour les détails.</p>
</li>
<li>
<p><a href="https://github.com/sharpbracket/redshift">Redshift</a> ajuste la température des couleurs de l’écran en
  fonction de l’heure de la journée.</p>
</li>
<li>
<p><a href="https://github.com/naelstrof/maim">maim</a> est un utilitaire pour faire des captures d’écran.
  J’utilise <kbd>Prt Scn</kbd> pour déclencher une capture d’écran
  d’une fenêtre ou d’une zone spécifique et <kbd>Mod</kbd>+<kbd>Prt
  Scn</kbd> pour capturer le bureau entier dans un fichier. Consultez
  le <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/screenshot">script associé</a> pour plus de détails.</p>
</li>
<li>
<p>J’ai une collection de fonds d’écran que je fais tourner toutes les
  heures. Un <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/wallpaper">script</a> les sélectionne avec un algorithme
  neuronal particulièrement élaboré et les assemble sur les
  configurations multi-écrans. Le fond d’écran sélectionné est
  réutilisé par <em>i3lock</em>.</p>
</li>
</ul>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>Cet article a été abondamment discuté sur <a href="https://news.ycombinator.com/item?id=28430849">Hacker News</a> et <a href="https://lobste.rs/s/5jdjno/switching_i3_window_manager">Lobsters</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:tearfree">
<p>Outre le côté esthétique, un compositeur permet également
de fluidifier le rendu vidéo. <a class="footnote-backref" href="#fnref:tearfree" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:intel">
<p>Ma configuration fonctionne pour les cartes graphiques Intel
des générations Haswell (2014) et Whiskey Lake (2018). Elle
fonctionne aussi pour les cartes AMD de la génération Polaris
(2017) <a class="footnote-backref" href="#fnref:intel" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:limitation">
<p>Il n’est pas possible de gérer plusieurs serveurs X de
cette façon, par exemple <code>:0</code> et <code>:1</code>. Dans une première
implémentation, j’ai essayé de paramétrer chaque service avec le
serveur associé, mais c’est inutile : il n’y a qu’une seule
session utilisateur DBus et de nombreux services en dépendent. Par
exemple, il n’est pas possible d’exécuter deux démons de
notification. <a class="footnote-backref" href="#fnref:limitation" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:dimmer">
<p>Je n’ai découvert que plus tard que <a href="https://github.com/google/xsecurelock" title="X11 screen lock utility with security in mind">XSecureLock</a> livre
une telle application avec une implémentation similaire. Mais le
mien a un compte à rebours ! Il permet également de faire un fondu
vers une image de fond. <a class="footnote-backref" href="#fnref:dimmer" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Jerikan+Ansible : un système de gestion de configuration pour le réseau</title>
    <author><name>ex-Blade Network Team</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2021-reseau-jerikan-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2021-reseau-jerikan-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-05-25T12:16:03Z</updated>
    <id>http://www.luffy.cx/fr/blog/2021-reseau-jerikan-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Il existe de nombreuses ressources pour l’automatisation du réseau
avec <a href="https://www.ansible.com" title="https://www.ansible.com">Ansible</a>. La plupart d’entre elles n’exposent que les
<a href="https://docs.ansible.com/projects/ansible/latest/network/getting_started/index.html" title="Ansible for Network Automation">premières étapes</a> ou se limitent à un <a href="https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules" title="NVIDIA Cumulus Linux Roles">champ
d’application réduit</a>. Elles donnent peu d’indications
sur la manière d’aller plus loin. Les environnements réseau
réels peuvent être vastes, polyvalents, hétérogènes et remplis
d’exceptions. L’absence d’exemples concrets de déploiements Ansible,
contrairement à <a href="https://github.com/wikimedia/operations-puppet" title="Puppet repository for the Wikimedia Foundation">Puppet</a> et <a href="https://github.com/creativecommons/sre-salt-prime" title="Creative Commons SaltStack configuration files">SaltStack</a>, conduit de nombreuses
équipes à élaborer des solutions fragiles et incomplètes.</p>
<p>Nous avons <a href="https://github.com/jerikan-network/cmdb">publié</a> sous une licence libre notre tentative
pour pallier ce manque :</p>
<ul>
<li><strong>Jerikan</strong>, un outil pour construire des fichiers de configuration
   à partir d’une <em>source de vérité unique</em> et de <em>modèles
   <a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a></em>, ainsi que son intégration dans <em>GitLab</em> ;</li>
<li>un <strong>playbook Ansible</strong> pour déployer ces fichiers de configuration
   sur les équipements réseau ;</li>
<li>les <strong>données de configuration et les modèles</strong> pour nos centres de
   données, aujourd’hui fermés, à San Francisco et en Corée du Sud,
   couvrant de nombreux fournisseurs (Facebook Wedge 100, Dell S4048
   et S6010, Juniper QFX 5110, Juniper QFX 10002, Cisco ASR 9001,
   Cisco Catalyst 2960, serveurs de console Opengear et serveurs
   Linux) et de nombreuses fonctionnalités (approvisionnement, routage
   BGP vers l’hôte, routage Internet, réseau d’administration séparé,
   configuration DNS, intégration avec NetBox et les <abbr title="Internet Routing Registry">IRR</abbr>).</li>
</ul>
<p>Voici une démo pour l’ajout d’un nouveau peering:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1920px"><span class="lf-media-inner" style="padding-bottom: 56.250%"><video width="1920" height="1080" controls="" preload="none" crossorigin="anonymous" poster="https://media-bernat-ch.b-cdn.net/images/posters/2021-network-cmdb.4a4d44cee5dd6e.jpg" class="lf-media lf-opaque"><source src="https://media.bernat.ch/videos/2021-network-cmdb.m3u8" type="application/vnd.apple.mpegurl"/><source src="https://media.bernat.ch/videos/2021-network-cmdb/progressive.mp4" type="video/mp4; codecs=&quot;mp4a.40.2,avc1.4d401f&quot;"/><track src="https://media.bernat.ch/videos/2021-network-cmdb.en.vtt" kind="subtitles" srclang="en" label="English"/><track src="https://media.bernat.ch/videos/2021-network-cmdb.fr.vtt" kind="subtitles" srclang="fr" default="" label="français"/></video></span></div></figure>
<p>Ce travail est l’œuvre collective de <a href="https://www.linkedin.com/in/c%C3%A9dric-hasco%C3%ABt-5069087b/">Cédric Hascoët</a>,
<a href="https://www.linkedin.com/in/jean-christophe-legatte-b9b86298/">Jean-Christophe Legatte</a>, <a href="https://www.linkedin.com/in/lo%C3%AFc-pailhas-b40893136/">Loïc Pailhas</a>, <a href="https://www.linkedin.com/in/s%C3%A9bastien-hurtel-14a501140/">Sébastien Hurtel</a>,
<a href="https://www.linkedin.com/in/tchadelicard/">Tchadel Icard</a> et <a href="/fr">Vincent Bernat</a>. Nous sommes l’équipe réseau
de Blade, une société française exploitant <a href="https://shadow.tech">Shadow</a>, un produit de
PC dans les nuages. En mai 2021, notre société a été <a href="https://next.ink/5763/reprise-blade-octave-klaba-emporte/" title="Reprise de Blade (Shadow) : Octave Klaba l'emporte">rachetée par
Octave Klaba</a> et l’infrastructure est en
cours de transfert vers <a href="https://www.ovhcloud.com">OVHcloud</a>, sauvant ainsi <em>Shadow</em> en tant
que produit, mais rendant notre équipe superflue.</p>
<p>Notre réseau comptait environ 800 équipements, répartis sur 10 centres
de données avec plus de 2,5 Tbps de bande passante disponible en
sortie. Le matériel publié est donc un exemple substantiel de gestion
d’un réseau de taille moyenne à l’aide d’<em>Ansible</em>. Nous avons laissé
de côté la configuration de nos centres de données plus anciens pour
rendre le résultat final plus lisible, tout en conservant suffisamment
de substance pour ne pas en faire un exemple trivial.</p>
<div class="toc">
<ul>
<li><a href="#jerikan">Jerikan</a><ul>
<li><a href="#source-de-verite">Source de vérité</a></li>
<li><a href="#modeles">Modèles</a></li>
<li><a href="#validation">Validation</a></li>
<li><a href="#integration-continue-avec-gitlab">Intégration continue avec GitLab</a></li>
</ul>
</li>
<li><a href="#ansible">Ansible</a><ul>
<li><a href="#inventaire">Inventaire</a></li>
<li><a href="#playbook">Playbook</a></li>
</ul>
</li>
<li><a href="#donnees-de-configuration-et-modeles">Données de configuration et modèles</a></li>
</ul>
</div>
<h1 id="jerikan">Jerikan</h1>
<p>Le premier composant est <em>Jerikan</em>. En entrée, il prend une liste
d’équipements, des données de configuration, des modèles et des
scripts de validation. Il génère un ensemble de fichiers de
configuration pour chaque appareil. <em>Ansible</em> pourrait effectuer cette
tâche, mais il présente les limitations suivantes concernant la
compilation des modèles :</p>
<ul>
<li>il est lent ;</li>
<li>les erreurs sont difficiles à corriger<sup id="fnref:debug"><a class="footnote-ref" href="#fn:debug">1</a></sup> ;</li>
<li>la hiérarchie de recherche d’une variable est <a href="https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence" title="Using Variables: Understanding variable precedence">rigide</a>.</li>
</ul>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 830px"><span class="lf-media-inner" style="padding-bottom: 71.807%"><img alt="Entrées et sorties de Jerikan" src="https://media-bernat-ch.b-cdn.net/images/jerikan/jerikan.41bd9d738ac96c.svg" width="830" height="596" class="lf-media"/></span></div><figcaption>Entrées et sorties de Jerikan</figcaption></figure>
<p>Si vous voulez suivre les exemples, il vous suffit d’avoir installé
<a href="https://docs.docker.com/" title="Docker documentation">Docker</a> et <a href="https://docs.docker.com/compose/" title="Overview of Docker Compose">Docker Compose</a>. Clonez le <a href="https://github.com/jerikan-network/cmdb">dépôt</a> et vous
êtes prêt !</p>
<h2 id="source-de-verite">Source de vérité</h2>
<p>Nous utilisons des fichiers YAML, versionnés avec <em>Git</em>, comme source
de vérité unique au lieu d’utiliser une base de données, comme NetBox,
ou un mélange de base de données et de fichiers texte. Cela offre de
nombreux avantages :</p>
<ul>
<li>chacun peut utiliser son éditeur de texte préféré ;</li>
<li>l’équipe prépare les changements dans des branches ;</li>
<li>l’équipe évalue les changements en utilisant des <em>demandes de fusion</em> ;</li>
<li>les demandes de fusion présentent l’impact sur les fichiers de configuration générés ;</li>
<li>le retour à un état antérieur est facile ;</li>
<li>c’est rapide.</li>
</ul>
<div class="admonition">
<p class="admonition-title">Mise à jour (11.2021)</p>
<p>Pour un argumentaire plus détaillé sur le choix de <em>Git</em> par rapport à
<em>NetBox</em>, lisez « <a href="/fr/blog/2021-source-verite-reseau">Git comme source de vérité pour l’automatisation du
réseau</a> »</p>
</div>
<p>Le premier fichier est <a href="https://github.com/jerikan-network/cmdb/blob/main/devices.yaml"><code>devices.yaml</code></a>. Il contient la
liste des équipements. Le deuxième fichier est
<a href="https://github.com/jerikan-network/cmdb/blob/main/classifier.yaml"><code>classifier.yaml</code></a>. Il définit un ensemble de
clés-valeurs pour chaque équipement. Il est utilisé dans les modèles
et pour rechercher les données associées à un appareil.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>scope<span class="w"> </span>to1-p1.sk1.blade-group.net
<span class="go">continent: apac</span>
<span class="go">environment: prod</span>
<span class="go">groups:</span>
<span class="go">- tor</span>
<span class="go">- tor-bgp</span>
<span class="go">- tor-bgp-compute</span>
<span class="go">host: to1-p1.sk1</span>
<span class="go">location: sk1</span>
<span class="go">member: '1'</span>
<span class="go">model: dell-s4048</span>
<span class="go">os: cumulus</span>
<span class="go">pod: '1'</span>
<span class="go">shorthost: to1-p1</span>
</code></pre></div>

<p>Le nom de l’équipement est comparé à une liste d’expressions
régulières et chaque correspondance contribue au résultat. Pour
<code>to1-p1.sk1.blade-group.net</code>, le sous-ensemble suivant de
<code>classifier.yaml</code> définit ses variables :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">matchers</span><span class="p">:</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'^(([^.]*)\..*)\.blade-group\.net'</span><span class="p p-Indicator">:</span>
<span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">prod</span>
<span class="w">      </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="s">'\1'</span>
<span class="w">      </span><span class="nt">shorthost</span><span class="p">:</span><span class="w"> </span><span class="s">'\2'</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'\.(sk1)\.'</span><span class="p p-Indicator">:</span>
<span class="w">      </span><span class="nt">location</span><span class="p">:</span><span class="w"> </span><span class="s">'\1'</span>
<span class="w">      </span><span class="nt">continent</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">apac</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'^to([12])-[as]?p(\d+)\.'</span><span class="p p-Indicator">:</span>
<span class="w">      </span><span class="nt">member</span><span class="p">:</span><span class="w"> </span><span class="s">'\1'</span>
<span class="w">      </span><span class="nt">pod</span><span class="p">:</span><span class="w"> </span><span class="s">'\2'</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'^to[12]-p\d+\.'</span><span class="p p-Indicator">:</span>
<span class="w">      </span><span class="nt">groups</span><span class="p">:</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tor</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tor-bgp</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tor-bgp-compute</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'^to[12]-(p|ap)\d+\.sk1\.'</span><span class="p p-Indicator">:</span>
<span class="w">      </span><span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">cumulus</span>
<span class="w">      </span><span class="nt">model</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">dell-s4048</span>
</code></pre></div>

<p>Le troisième fichier est <a href="https://github.com/jerikan-network/cmdb/blob/main/searchpaths.py"><code>searchpaths.py</code></a>. Il décrit
les répertoires à examiner pour trouver une variable. Une fonction
Python fournit une liste de chemins dans <a href="https://github.com/jerikan-network/cmdb/tree/main/data"><code>data/</code></a> selon les
variables associées à l’équipement. Voici une version
simplifiée<sup id="fnref:jerakia"><a class="footnote-ref" href="#fn:jerakia">2</a></sup> :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">def</span><span class="w"> </span><span class="nf">searchpaths</span><span class="p">(</span><span class="n">scope</span><span class="p">):</span>
    <span class="n">paths</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">"host/</span><span class="si">{scope[location]}</span><span class="s2">/</span><span class="si">{scope[shorthost]}</span><span class="s2">"</span><span class="p">,</span>
        <span class="s2">"location/</span><span class="si">{scope[location]}</span><span class="s2">"</span><span class="p">,</span>
        <span class="s2">"os/</span><span class="si">{scope[os]}</span><span class="s2">-</span><span class="si">{scope[model]}</span><span class="s2">"</span><span class="p">,</span>
        <span class="s2">"os/</span><span class="si">{scope[os]}</span><span class="s2">"</span><span class="p">,</span>
        <span class="s1">'common'</span>
    <span class="p">]</span>
    <span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">paths</span><span class="p">)):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">paths</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">=</span> <span class="n">paths</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="n">scope</span><span class="p">)</span>
        <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
            <span class="n">paths</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">path</span> <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">paths</span> <span class="k">if</span> <span class="n">path</span><span class="p">]</span>
</code></pre></div>

<p>Avec cette définition, les données pour <code>to1-p1.sk1.blade-group.net</code>
sont recherchées dans les chemins suivants :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>scope<span class="w"> </span>to1-p1.sk1.blade-group.net
<span class="go">[…]</span>
<span class="go">Search paths:</span>
<span class="go">  host/sk1/to1-p1</span>
<span class="go">  location/sk1</span>
<span class="go">  os/cumulus-dell-s4048</span>
<span class="go">  os/cumulus</span>
<span class="go">  common</span>
</code></pre></div>

<p>Les variables sont limitées à un espace de noms à spécifier lors d’une
recherche. Nous utilisons les suivants :</p>
<ul>
<li><code>system</code> pour les comptes, DNS, serveurs syslog ;</li>
<li><code>topology</code> pour les ports, interfaces, adresses IP, sous-réseaux ;</li>
<li><code>bgp</code> pour la configuration de BGP ;</li>
<li><code>build</code> pour les modèles et les scripts de validation ;</li>
<li><code>apps</code> pour les variables d’application.</li>
</ul>
<p>Lors de la recherche d’une variable dans un espace de noms donné,
<em>Jerikan</em> recherche un fichier YAML nommé d’après l’espace de noms
dans chaque répertoire retourné par la fonction de recherche. Par
exemple, si nous recherchons une variable pour
<code>to1-p1.sk1.blade-group.net</code> dans l’espace de noms <code>bgp</code>, les fichiers
YAML suivants sont traités : <code>host/sk1/to1-p1/bgp.yaml</code>,
<code>location/sk1/bgp.yaml</code>, <code>os/cumulus-dell-s4048/bgp.yaml</code>,
<code>os/cumulus/bgp.yaml</code> et <code>common/bgp.yaml</code>. La recherche s’arrête à
la première correspondance.</p>
<p>Le fichier <a href="https://github.com/jerikan-network/cmdb/blob/main/schema.yaml"><code>schema.yaml</code></a> nous permet d’outrepasser ce
dernier comportement en fusionnant dans certains cas les dictionnaires
et les tableaux correspondants<sup id="fnref:schema"><a class="footnote-ref" href="#fn:schema">3</a></sup>. Voici un extrait de ce
fichier pour l’espace de noms <code>topology</code> :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">system</span><span class="p">:</span>
<span class="w">  </span><span class="nt">users</span><span class="p">:</span>
<span class="w">    </span><span class="nt">merge</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">hash</span>
<span class="w">  </span><span class="nt">sampling</span><span class="p">:</span>
<span class="w">    </span><span class="nt">merge</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">hash</span>
<span class="w">  </span><span class="nt">ansible-vars</span><span class="p">:</span>
<span class="w">    </span><span class="nt">merge</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">hash</span>
<span class="w">  </span><span class="nt">netbox</span><span class="p">:</span>
<span class="w">    </span><span class="nt">merge</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">hash</span>
</code></pre></div>

<p>La dernière fonctionnalité de la source de vérité est la possibilité
d’utiliser des modèles <a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a> pour les clefs et les valeurs en les
préfixant par « <code>~</code> » :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="c1"># Dans data/os/junos/system.yaml</span>
<span class="nt">netbox</span><span class="p">:</span>
<span class="w">  </span><span class="nt">manufacturer</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Juniper</span>
<span class="w">  </span><span class="nt">model</span><span class="p">:</span><span class="w"> </span><span class="s">"~{{</span><span class="nv"> </span><span class="s">model|upper</span><span class="nv"> </span><span class="s">}}"</span>

<span class="c1"># Dans data/groups/tor-bgp-compute/system.yaml</span>
<span class="nt">netbox</span><span class="p">:</span>
<span class="w">  </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">net_tor_gpu_switch</span>
</code></pre></div>

<p>Chercher <code>netbox</code> dans l’espace de noms <code>system</code> pour
<code>to1-p2.ussfo03.blade-group.net</code> produit le résultat suivant :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>scope<span class="w"> </span>to1-p2.ussfo03.blade-group.net
<span class="go">continent: us</span>
<span class="go">environment: prod</span>
<span class="go">groups:</span>
<span class="go">- tor</span>
<span class="go">- tor-bgp</span>
<span class="go">- tor-bgp-compute</span>
<span class="go">host: to1-p2.ussfo03</span>
<span class="go">location: ussfo03</span>
<span class="go">member: '1'</span>
<span class="go">model: qfx5110-48s</span>
<span class="go">os: junos</span>
<span class="go">pod: '2'</span>
<span class="go">shorthost: to1-p2</span>
<span class="go">[…]</span>
<span class="go">Search paths:</span>
<span class="go">[…]</span>
<span class="go">  groups/tor-bgp-compute</span>
<span class="go">[…]</span>
<span class="go">  os/junos</span>
<span class="go">  common</span>
<span class="gp">$ </span>./run-jerikan<span class="w"> </span>lookup<span class="w"> </span>to1-p2.ussfo03.blade-group.net<span class="w"> </span>system<span class="w"> </span>netbox
<span class="go">manufacturer: Juniper</span>
<span class="go">model: QFX5110-48S</span>
<span class="go">role: net_tor_gpu_switch</span>
</code></pre></div>

<p>Cela fonctionne aussi pour des données structurées :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="c1"># Dans groups/adm-gateway/topology.yaml</span>
<span class="nt">interface-rescue</span><span class="p">:</span>
<span class="w">  </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span><span class="s">"~{{</span><span class="nv"> </span><span class="s">lookup('topology',</span><span class="nv"> </span><span class="s">'addresses').rescue</span><span class="nv"> </span><span class="s">}}"</span>
<span class="w">  </span><span class="nt">up</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">"~ip</span><span class="nv"> </span><span class="s">route</span><span class="nv"> </span><span class="s">add</span><span class="nv"> </span><span class="s">default</span><span class="nv"> </span><span class="s">via</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">lookup('topology',</span><span class="nv"> </span><span class="s">'addresses').rescue|ipaddr('first_usable')</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">table</span><span class="nv"> </span><span class="s">rescue"</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">"~ip</span><span class="nv"> </span><span class="s">rule</span><span class="nv"> </span><span class="s">add</span><span class="nv"> </span><span class="s">from</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">lookup('topology',</span><span class="nv"> </span><span class="s">'addresses').rescue|ipaddr('address')</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">table</span><span class="nv"> </span><span class="s">rescue</span><span class="nv"> </span><span class="s">priority</span><span class="nv"> </span><span class="s">10"</span>

<span class="c1"># Dans groups/adm-gateway-sk1/topology.yaml</span>
<span class="nt">interfaces</span><span class="p">:</span>
<span class="w">  </span><span class="nt">ens1f0</span><span class="p">:</span><span class="w"> </span><span class="s">"~{{</span><span class="nv"> </span><span class="s">lookup('topology',</span><span class="nv"> </span><span class="s">'interface-rescue')</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div>

<p>Cela retourne le résultat suivant :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>lookup<span class="w"> </span>gateway1.sk1.blade-group.net<span class="w"> </span>topology<span class="w"> </span>interfaces
<span class="go">[…]</span>
<span class="go">ens1f0:</span>
<span class="go">  address: 121.78.242.10/29</span>
<span class="go">  up:</span>
<span class="go">  - ip route add default via 121.78.242.9 table rescue</span>
<span class="go">  - ip rule add from 121.78.242.10 table rescue priority 10</span>
</code></pre></div>

<p>Pour placer des données dans la source de vérité, nous utilisons les
règles suivantes :</p>
<ol>
<li>Ne pas se répéter.</li>
<li>Préférer l’emplacement le plus spécifique sans enfreindre la première règle.</li>
<li>Utiliser avec parcimonie les modèles, principalement pour respecter les règles précédentes.</li>
<li>Limiter le modèle de données à ce qui est nécessaire pour votre cas d’utilisation.</li>
</ol>
<p>La première règle est assez importante. Par exemple, lorsque vous
spécifiez des adresses IP pour une liaison point à point, ne spécifiez
qu’un côté et déduisez l’autre valeur via les modèles. La dernière
règle signifie qu’il est inutile de reproduire un <a href="https://github.com/openconfig/public/tree/master/release/models/bgp" title="OpenConfig's YANG model for BGP">modèle BGP
YANG</a> pour spécifier les pairs et les politiques BGP :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">peers</span><span class="p">:</span>
<span class="w">  </span><span class="nt">transit</span><span class="p">:</span>
<span class="w">    </span><span class="nt">cogent</span><span class="p">:</span>
<span class="w">      </span><span class="nt">asn</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">174</span>
<span class="w">      </span><span class="nt">remote</span><span class="p">:</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">38.140.30.233</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2001:550:2:B::1F9:1</span>
<span class="w">      </span><span class="nt">specific-import</span><span class="p">:</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">ATT-US</span>
<span class="w">          </span><span class="nt">as-path</span><span class="p">:</span><span class="w"> </span><span class="s">".*7018$"</span>
<span class="w">          </span><span class="nt">lp-delta</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">50</span>
<span class="w">  </span><span class="nt">ix-sfmix</span><span class="p">:</span>
<span class="w">    </span><span class="nt">rs-sfmix</span><span class="p">:</span>
<span class="w">      </span><span class="nt">monitored</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
<span class="w">      </span><span class="nt">asn</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">63055</span>
<span class="w">      </span><span class="nt">remote</span><span class="p">:</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">206.197.187.253</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">206.197.187.254</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2001:504:30::ba06:3055:1</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2001:504:30::ba06:3055:2</span>
<span class="w">    </span><span class="nt">blizzard</span><span class="p">:</span>
<span class="w">      </span><span class="nt">asn</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">57976</span>
<span class="w">      </span><span class="nt">remote</span><span class="p">:</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">206.197.187.42</span>
<span class="w">        </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2001:504:30::ba05:7976:1</span>
<span class="w">      </span><span class="nt">irr</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">AS-BLIZZARD</span>
</code></pre></div>

<h2 id="modeles">Modèles</h2>
<p>La liste des modèles à compiler pour chaque équipement est stockée
dans la source de vérité, sous l’espace de noms <code>build</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>lookup<span class="w"> </span>edge1.ussfo03.blade-group.net<span class="w"> </span>build<span class="w"> </span>templates
<span class="go">data.yaml: data.j2</span>
<span class="go">config.txt: junos/main.j2</span>
<span class="go">config-base.txt: junos/base.j2</span>
<span class="go">config-irr.txt: junos/irr.j2</span>
<span class="gp">$ </span>./run-jerikan<span class="w"> </span>lookup<span class="w"> </span>to1-p1.ussfo03.blade-group.net<span class="w"> </span>build<span class="w"> </span>templates
<span class="go">data.yaml: data.j2</span>
<span class="go">config.txt: cumulus/main.j2</span>
<span class="go">frr.conf: cumulus/frr.j2</span>
<span class="go">interfaces.conf: cumulus/interfaces.j2</span>
<span class="go">ports.conf: cumulus/ports.j2</span>
<span class="go">dhcpd.conf: cumulus/dhcp.j2</span>
<span class="go">default-isc-dhcp: cumulus/default-isc-dhcp.j2</span>
<span class="go">authorized_keys: cumulus/authorized-keys.j2</span>
<span class="go">motd: linux/motd.j2</span>
<span class="go">acl.rules: cumulus/acl.j2</span>
<span class="go">rsyslog.conf: cumulus/rsyslog.conf.j2</span>
</code></pre></div>

<p>Les modèles utilisent <a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a>. C’est le même moteur que pour
<em>Ansible</em>. <em>Jerikan</em> fournit quelques filtres personnalisés mais
réutilise également certains des filtres d’<em>Ansible</em><sup id="fnref:extend"><a class="footnote-ref" href="#fn:extend">4</a></sup>,
notamment <a href="https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_filters_ipaddr.html" title="Ansible: ipaddr filter"><code>ipaddr</code></a>. Voici un extrait de
<a href="https://github.com/jerikan-network/cmdb/blob/main/templates/junos/base.j2"><code>templates/junos/base.j2</code></a> pour configurer
les serveurs DNS et NTP sur les équipements Juniper :</p>
<div class="language-django/jinja codehilite"><pre><span/><code><span class="x">system {</span>
<span class="x">  ntp {</span>
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">ntp</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s2">"system"</span><span class="o">,</span> <span class="s2">"ntp"</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">    server </span><span class="cp">{{</span> <span class="nv">ntp</span> <span class="cp">}}</span><span class="x">;</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="x">  }</span>
<span class="x">  name-server {</span>
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">dns</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s2">"system"</span><span class="o">,</span> <span class="s2">"dns"</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">    </span><span class="cp">{{</span> <span class="nv">dns</span> <span class="cp">}}</span><span class="x">;</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="x">  }</span>
<span class="x">}</span>
</code></pre></div>

<p>Voici le modèle équivalent pour Cisco IOS XR :</p>
<div class="language-django/jinja codehilite"><pre><span/><code><span class="cp">{%</span> <span class="k">for</span> <span class="nv">dns</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s1">'system'</span><span class="o">,</span> <span class="s1">'dns'</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">domain vrf VRF-MANAGEMENT name-server </span><span class="cp">{{</span> <span class="nv">dns</span> <span class="cp">}}</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="x">!</span>
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">syslog</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s1">'system'</span><span class="o">,</span> <span class="s1">'syslog'</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">logging </span><span class="cp">{{</span> <span class="nv">syslog</span> <span class="cp">}}</span><span class="x"> vrf VRF-MANAGEMENT</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="x">!</span>
</code></pre></div>

<p>Trois fonctions utiles sont fournies :</p>
<ul>
<li><code>devices()</code> renvoie la liste des équipements correspondant à un
   ensemble de conditions. Par exemple, <code>devices("location==ussfo03",
   "groups==tor-bgp")</code> renvoie la liste des routeurs de San Francisco
   appartenant au groupe <code>tor-bgp</code>. Vous pouvez également omettre
   l’opérateur si vous souhaitez que la valeur spécifiée soit égale à
   celle de l’équipement en cours. Par exemple, <code>devices("location")</code>
   renvoie les équipements situés dans le lieu actuel.</li>
<li><code>lookup()</code> effectue une recherche par clé. Elle prend l’espace de
   noms, la clé, et optionnellement, un nom d’équipement. S’il n’est
   pas fourni, la recherche s’effectue sur l’équipement actuel.</li>
<li><code>scope()</code> renvoie les variables associées à l’équipement indiqué.</li>
</ul>
<p>Voici comment définir des sessions iBGP entre des routeurs situés dans
le même centre de données :</p>
<div class="language-django/jinja codehilite"><pre><span/><code><span class="cp">{%</span> <span class="k">for</span> <span class="nv">neighbor</span> <span class="k">in</span> <span class="nv">devices</span><span class="o">(</span><span class="s2">"location"</span><span class="o">,</span> <span class="s2">"groups==edge"</span><span class="o">)</span> <span class="k">if</span> <span class="nv">neighbor</span> <span class="o">!=</span> <span class="nv">device</span> <span class="cp">%}</span>
<span class="x">  </span><span class="cp">{%</span> <span class="k">for</span> <span class="nv">address</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s2">"topology"</span><span class="o">,</span> <span class="s2">"addresses"</span><span class="o">,</span> <span class="nv">neighbor</span><span class="o">)</span><span class="nv">.loopback</span><span class="o">|</span><span class="nf">tolist</span> <span class="cp">%}</span>
<span class="x">protocols bgp group IPV</span><span class="cp">{{</span> <span class="nv">address</span><span class="o">|</span><span class="nf">ipv</span> <span class="cp">}}</span><span class="x">-EDGES-IBGP {</span>
<span class="x">  neighbor </span><span class="cp">{{</span> <span class="nv">address</span> <span class="cp">}}</span><span class="x"> {</span>
<span class="x">    description "IPv</span><span class="cp">{{</span> <span class="nv">address</span><span class="o">|</span><span class="nf">ipv</span> <span class="cp">}}</span><span class="x">: iBGP to </span><span class="cp">{{</span> <span class="nv">neighbor</span> <span class="cp">}}</span><span class="x">";</span>
<span class="x">  }</span>
<span class="x">}</span>
<span class="x">  </span><span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
</code></pre></div>

<p>Nous disposons également d’une base de données clé-valeur pour
enregistrer les informations à réutiliser dans un autre modèle ou
équipement. Ceci est très utile pour construire automatiquement des
enregistrement DNS. Tout d’abord, on « capture » l’adresse IP insérée
dans un modèle avec le filtre <code>store()</code> :</p>
<div class="language-django/jinja codehilite"><pre><span/><code><span class="x">interface Loopback0</span>
<span class="x"> description 'Loopback:'</span>
<span class="x"> </span><span class="cp">{%</span> <span class="k">for</span> <span class="nv">address</span> <span class="k">in</span> <span class="nv">lookup</span><span class="o">(</span><span class="s1">'topology'</span><span class="o">,</span> <span class="s1">'addresses'</span><span class="o">)</span><span class="nv">.loopback</span><span class="o">|</span><span class="nf">tolist</span> <span class="cp">%}</span>
<span class="x"> ipv</span><span class="cp">{{</span> <span class="nv">address</span><span class="o">|</span><span class="nf">ipv</span> <span class="cp">}}</span><span class="x"> address </span><span class="cp">{{</span> <span class="nv">address</span><span class="o">|</span><span class="nf">store</span><span class="o">(</span><span class="s1">'addresses'</span><span class="o">,</span> <span class="s1">'Loopback0'</span><span class="o">)|</span><span class="nf">ipaddr</span><span class="o">(</span><span class="s1">'cidr'</span><span class="o">)</span> <span class="cp">}}</span>
<span class="x"> </span><span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="x">!</span>
</code></pre></div>

<p>Puis, nous réutilisons les valeurs stockées pour construire les
enregistrements DNS en itérant sur les résultats de la fonction
<code>store()</code><sup id="fnref:function"><a class="footnote-ref" href="#fn:function">5</a></sup> :</p>
<div class="language-django/jinja codehilite"><pre><span/><code><span class="cp">{%</span> <span class="k">for</span> <span class="nv">device</span><span class="o">,</span> <span class="nv">ip</span><span class="o">,</span> <span class="nv">interface</span> <span class="k">in</span> <span class="nv">store</span><span class="o">(</span><span class="s1">'addresses'</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">  </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">interface</span> <span class="o">=</span> <span class="nv">interface</span><span class="o">|</span><span class="nf">replace</span><span class="o">(</span><span class="s1">'/'</span><span class="o">,</span> <span class="s1">'-'</span><span class="o">)|</span><span class="nf">replace</span><span class="o">(</span><span class="s1">'.'</span><span class="o">,</span> <span class="s1">'-'</span><span class="o">)|</span><span class="nf">replace</span><span class="o">(</span><span class="s1">':'</span><span class="o">,</span> <span class="s1">'-'</span><span class="o">)</span> <span class="cp">%}</span>
<span class="x">  </span><span class="cp">{%</span> <span class="k">set</span> <span class="nv">name</span> <span class="o">=</span> <span class="s1">'{}.{}'</span><span class="nv">.format</span><span class="o">(</span><span class="nv">interface</span><span class="o">|</span><span class="nf">lower</span><span class="o">,</span> <span class="nv">device</span><span class="o">)</span> <span class="cp">%}</span>
<span class="cp">{{</span> <span class="nv">name</span> <span class="cp">}}</span><span class="x">. IN </span><span class="cp">{{</span> <span class="s1">'A'</span> <span class="k">if</span> <span class="nv">ip</span><span class="o">|</span><span class="nf">ipv4</span> <span class="k">else</span> <span class="s1">'AAAA'</span> <span class="cp">}}</span><span class="x"> </span><span class="cp">{{</span> <span class="nv">ip</span><span class="o">|</span><span class="nf">ipaddr</span><span class="o">(</span><span class="s1">'address'</span><span class="o">)</span> <span class="cp">}}</span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
</code></pre></div>

<p>Les modèles sont compilés localement avec <code>./run-jerikan build</code>.
L’argument <code>--limit</code> restreint les équipements pour lesquels générer
des configurations. La construction ne se fait pas en parallèle car un
modèle peut dépendre des données collectées par un autre modèle.
Actuellement, il faut 1 minute pour construire environ 3000 fichiers
couvrant plus de 800 appareils.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 766px"><span class="lf-media-inner" style="padding-bottom: 47.389%"><img alt="Sortie de Jerikan lors de la compilation des&#10;modèles" src="https://media-bernat-ch.b-cdn.net/images/jerikan/templates@1x.88119fdd4a6124.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/jerikan/templates@1x.88119fdd4a6124.jpg 963w,https://media-bernat-ch.b-cdn.net/images/jerikan/templates@2x.c7fae982e002b2.jpg 1532w" sizes="auto, (max-width: 766px) 100vw, 766px" width="766" height="363" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABfwAAALXAQMAAADixhO6AAAAA1BMVEUZFgyho1zZAAAAnklEQVR42u3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHBqJDUAAb9XIogAAAAASUVORK5CYII=)"/></span></div><figcaption>Sortie de Jerikan après la génération des fichiers de configuration pour six équipements</figcaption></figure>
<p>Lorsqu’une erreur se produit, une trace détaillée est affichée. Elle
inclut le nom du modèle, le numéro de ligne et la valeur de toutes les
variables visibles. C’est un gain de temps considérable par rapport à
<em>Ansible</em> !</p>
<div class="language-text-only codehilite"><pre><span/><code><span class="hll">templates/opengear/config.j2:15: in top-level template code
</span>    config.interfaces.{{ interface }}.netmask {{ infos.adddress | ipaddr("netmask") }}
        continent  = 'us'
        device     = 'con1-ag2.ussfo03.blade-group.net'
        environment = 'prod'
        host       = 'con1-ag2.ussfo03'
<span class="hll">        infos      = {'address': '172.30.24.19/21'}
</span>        interface  = 'wan'
        location   = 'ussfo03'
        loop       = &lt;LoopContext 1/2&gt;
        member     = '2'
        model      = 'cm7132-2-dac'
        os         = 'opengear'
        shorthost  = 'con1-ag2'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = JerkianUndefined, query = 'netmask', version = False, alias = 'ipaddr'

[…]
        # Check if value is a list and parse each element
        if isinstance(value, (list, tuple, types.GeneratorType)):
            _ret = [ipaddr(element, str(query), version) for element in value]
            return [item for item in _ret if item]

&gt;       elif not value or value is True:
<span class="hll">E       jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'adddress'
</span></code></pre></div>

<p>Nous ne disposons pas de règles générales pour l’écriture des modèles.
Comme pour la source de vérité, il n’est pas nécessaire de créer des
modèles génériques capables de produire n’importe quelle configuration
BGP. Il y a un équilibre à trouver entre la lisibilité et le fait
d’éviter la duplication. Les modèles peuvent devenir effrayants et
complexes : parfois, il est préférable d’écrire un filtre ou une
fonction dans <a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jinja.py"><code>jerikan/jinja.py</code></a>. Maîtriser
<a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a> est un très bon investissement. Prenez le temps de
parcourir <a href="https://github.com/jerikan-network/cmdb/tree/main/templates">nos modèles</a>, car certains d’entre eux
présentent des fonctionnalités intéressantes.</p>
<h2 id="validation">Validation</h2>
<p>Optionellement, chaque fichier de configuration peut être validé par
un script dans le répertoire <a href="https://github.com/jerikan-network/cmdb/tree/main/checks"><code>checks/</code></a>. <em>Jerikan</em> utilise la
variable <code>checks</code> dans l’espace de noms <code>build</code> pour connaître les
tests à appliquer :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-jerikan<span class="w"> </span>lookup<span class="w"> </span>edge1.ussfo03.blade-group.net<span class="w"> </span>build<span class="w"> </span>checks
<span class="go">- description: Juniper configuration file syntax check</span>
<span class="go">  script: checks/junoser</span>
<span class="go">  cache:</span>
<span class="go">    input: config.txt</span>
<span class="go">    output: config-set.txt</span>
<span class="go">- description: check YAML data</span>
<span class="go">  script: checks/data.yaml</span>
<span class="go">  cache: data.yaml</span>
</code></pre></div>

<p>Dans l’exemple ci-dessus, <code>checks/junoser</code> est exécuté s’il y a une
modification du fichier généré <code>config.txt</code>. Il produit également une
version transformée du fichier de configuration qui est plus facile à
interfacer avec l’utilitaire <code>diff</code>. <a href="https://github.com/codeout/junoser/" title="PEG parser for Junos configuration">Junoser</a> vérifie un fichier de
configuration Junos en utilisant la définition du schéma XML pour
Netconf<sup id="fnref:fork"><a class="footnote-ref" href="#fn:fork">6</a></sup>. En cas d’erreur, <em>Jerikan</em> affiche :</p>
<div class="language-text-only codehilite"><pre><span/><code>jerikan/build.py:127: RuntimeError
-------------- Captured syntax check with Junoser call --------------
P: checks/junoser edge2.ussfo03.blade-group.net
C: /app/jerikan
O:
E: Invalid syntax:  set system syslog archive size 10m files 10 word-readable
S: 1
</code></pre></div>

<h2 id="integration-continue-avec-gitlab">Intégration continue avec GitLab</h2>
<p>L’étape suivante consiste à compiler les modèles en utilisant un
système d’intégration continue. Comme nous utilisons GitLab, <em>Jerikan</em>
contient un fichier <a href="https://github.com/jerikan-network/cmdb/blob/main/.gitlab-ci.yml"><code>.gitlab-ci.yml</code></a>. Lorsque
nous devons effectuer un changement, nous créons une branche dédiée et
une demande de fusion. GitLab compile les modèles en utilisant le même
environnement que celui que nous utilisons sur nos ordinateurs
portables et les stocke dans un artefact.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 877px"><span class="lf-media-inner" style="padding-bottom: 83.010%"><img alt="Demande de fusion dans&#10;GitLab" src="https://media-bernat-ch.b-cdn.net/images/jerikan/merge-request@1x.b14959e54f8e5b.png" srcset="https://media-bernat-ch.b-cdn.net/images/jerikan/merge-request@1x.b14959e54f8e5b.png 1102w,https://media-bernat-ch.b-cdn.net/images/jerikan/merge-request@2x.807c548ac3be57.png 1754w" sizes="auto, (max-width: 877px) 100vw, 877px" width="877" height="728" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA20AAALYAQMAAADVTIj7AAAAA1BMVEX///+nxBvIAAAAZUlEQVR42u3BMQEAAADCoPVPbQdvoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgMO7cAAdTEtMgAAAAASUVORK5CYII=)"/></span></div><figcaption>Demande de fusion pour ajouter un nouveau port sur USSFO03. Les modèles sont compilés avec succès mais un autre membre de l'équipe doit approuver le changement avant de pouvoir l'intégrer.</figcaption></figure>
<p>Avant d’approuver une fusion, un autre membre de l’équipe étudie les
changements au niveau du code mais aussi les différences au niveau des
fichiers générés :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 502px"><span class="lf-media-inner" style="padding-bottom: 118.924%"><img alt="Différences sur les fichiers&#10;générés" src="https://media-bernat-ch.b-cdn.net/images/jerikan/diff@1x.c680da8b801175.png" srcset="https://media-bernat-ch.b-cdn.net/images/jerikan/diff@1x.c680da8b801175.png 631w,https://media-bernat-ch.b-cdn.net/images/jerikan/diff@2x.23671acf551d04.png 1004w" sizes="auto, (max-width: 502px) 100vw, 502px" width="502" height="597" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfYAAAJVAQMAAAAWRUUiAAAAA1BMVEX///+nxBvIAAAAPElEQVR42u3BgQAAAADDoPlTX+AIVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8A5VAAAGqYCW7AAAAAElFTkSuQmCC)"/></span></div><figcaption>Le changement inclut la configuration d'un port sur un équipement Juniper, l'ajout des enregistrements DNS et les mises à jour de NetBox pour les nouvelles adresses IP.</figcaption></figure>
<h1 id="ansible">Ansible</h1>
<p>Une fois que <em>Jerikan</em> a construit les fichiers de configuration,
<em>Ansible</em> prend le relais. Il est également livré sous la forme d’une
image Docker pour éviter la maintenance d’un environnement virtuel
Python et s’assurer que nous utilisons tous les mêmes versions.</p>
<h2 id="inventaire">Inventaire</h2>
<p><em>Jerikan</em> a généré un fichier d’inventaire. Il contient tous les
équipements gérés, des variables définies pour chacun d’eux et les
groupes convertis en groupes <em>Ansible</em> :</p>
<div class="language-toml codehilite"><pre><span/><code><span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span><span class="w"> </span><span class="n">ansible_host</span><span class="o">=</span><span class="mf">172.29</span><span class="p">.</span><span class="n">15</span><span class="p">.</span><span class="n">12</span><span class="w"> </span><span class="n">ansible_user</span><span class="o">=</span><span class="err">blade ansible</span><span class="mi">_</span><span class="n">connection</span><span class="o">=</span><span class="err">network</span><span class="mi">_</span><span class="n">cli</span><span class="w"> </span><span class="n">ansible_network_os</span><span class="o">=</span><span class="err">ios</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span><span class="w"> </span><span class="n">ansible_host</span><span class="o">=</span><span class="mf">172.29</span><span class="p">.</span><span class="n">15</span><span class="p">.</span><span class="n">13</span><span class="w"> </span><span class="n">ansible_user</span><span class="o">=</span><span class="err">blade ansible</span><span class="mi">_</span><span class="n">connection</span><span class="o">=</span><span class="err">network</span><span class="mi">_</span><span class="n">cli</span><span class="w"> </span><span class="n">ansible_network_os</span><span class="o">=</span><span class="err">ios</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span><span class="w"> </span><span class="n">ansible_host</span><span class="o">=</span><span class="mf">172.29</span><span class="p">.</span><span class="n">15</span><span class="p">.</span><span class="n">12</span><span class="w"> </span><span class="n">ansible_user</span><span class="o">=</span><span class="err">blade ansible</span><span class="mi">_</span><span class="n">connection</span><span class="o">=</span><span class="err">network</span><span class="mi">_</span><span class="n">cli</span><span class="w"> </span><span class="n">ansible_network_os</span><span class="o">=</span><span class="err">ios</span>
<span class="n">none</span><span class="w"> </span><span class="n">ansible_connection</span><span class="o">=</span><span class="err">local</span>

<span class="k">[oob]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>

<span class="k">[os-ios]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>

<span class="k">[model-c2960s]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>

<span class="k">[location-sk1]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>

<span class="k">[location-ussfo03]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>

<span class="k">[in-sync]</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob2-n1</span><span class="p">.</span><span class="n">sk1</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">ob1-n1</span><span class="p">.</span><span class="n">ussfo03</span><span class="p">.</span><span class="n">blade-group</span><span class="p">.</span><span class="n">net</span>
<span class="n">none</span>
</code></pre></div>

<p><code>in-sync</code> est un groupe spécial pour les équipements dont la
configuration doit correspondre à la configuration générée.
Quotidiennement et sans surveillance, <em>Ansible</em> devrait être capable
de pousser les configurations vers ce groupe. L’objectif à moyen terme
est de couvrir tous les équipements.</p>
<p><code>none</code> est un équipement spécial pour les tâches non liées à un hôte
spécifique. Cela inclut la <a href="/fr/blog/2020-synchro-netbox-ansible" title="Synchroniser NetBox avec un module Ansible sur mesure">synchronisation de NetBox</a>, des <a href="/fr/blog/2020-synchro-ripe-arin-apnic-ansible" title="Synchroniser des objets RIPE, ARIN et APNIC avec un module Ansible sur mesure">objets <abbr title="Internet Routing Registry">IRR</abbr></a> et du DNS, la mise à jour de la
<abbr title="Resource Public Key Infrastructure">RPKI</abbr> et la construction des fichiers de géolocalisation.</p>
<h2 id="playbook">Playbook</h2>
<p>Nous utilisons un unique <em>playbook</em> pour tous les équipements. Il est
décrit dans le fichier
<a href="https://github.com/jerikan-network/cmdb/blob/main/ansible/playbooks/site.yaml"><code>ansible/playbooks/site.yaml</code></a>. En voici
une version raccourcie :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">adm-gateway:!done</span>
<span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">mitogen_linear</span>
<span class="w">  </span><span class="nt">roles</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.linux</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.adm-gateway</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">done</span>

<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">os-linux:!done</span>
<span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">mitogen_linear</span>
<span class="w">  </span><span class="nt">roles</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.linux</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">done</span>

<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">os-junos:!done</span>
<span class="w">  </span><span class="nt">gather_facts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>
<span class="w">  </span><span class="nt">roles</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.junos</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">done</span>

<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">os-opengear:!done</span>
<span class="w">  </span><span class="nt">gather_facts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>
<span class="w">  </span><span class="nt">roles</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.opengear</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">done</span>

<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">none:!done</span>
<span class="w">  </span><span class="nt">gather_facts</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>
<span class="w">  </span><span class="nt">roles</span><span class="p">:</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">blade.none</span>
<span class="w">    </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">done</span>
</code></pre></div>

<p>Un hôte n’exécute qu’un seul des rôles. Par exemple, un équipement
Junos exécute le rôle <code>blade.junos</code>. Une fois qu’un rôle a été
exécuté, l’équipement est ajouté au groupe <code>done</code> et les autres rôles
sont ignorés.</p>
<p>Le playbook peut être exécuté avec les fichiers de configuration
générés par GitLab en utilisant la commande <code>./run-ansible-gitlab</code>.
Elle utilise Docker pour exposer la commande <code>ansible-playbook</code> et
elle accepte les mêmes arguments. Pour simuler un déploiement de la
configuration sur les équipements du centre de données SK1, nous
utilisons :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>./run-ansible-gitlab<span class="w"> </span>playbooks/site.yaml<span class="w"> </span>--limit<span class="o">=</span><span class="s1">'edge:&amp;location-sk1'</span><span class="w"> </span>--diff<span class="w"> </span>--check
<span class="go">[…]</span>
<span class="go">PLAY RECAP *************************************************************</span>
<span class="go">edge1.sk1.blade-group.net  : ok=6    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0</span>
<span class="go">edge2.sk1.blade-group.net  : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0</span>
</code></pre></div>

<p>Nous avons quelques règles pour écrire les rôles :</p>
<ul>
<li><code>--check</code> doit détecter si un changement est nécessaire,</li>
<li><code>--diff</code> doit fournir une visualisation des changements prévus,</li>
<li><code>--check</code> et <code>--diff</code> ne doivent rien afficher s’il n’y a rien à changer,</li>
<li><a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">écrire un module sur mesure</a> pour nos besoins est une solution valable,</li>
<li>toute la configuration de l’équipement est gérée<sup id="fnref:derivation"><a class="footnote-ref" href="#fn:derivation">7</a></sup>,</li>
<li>les secrets doivent être stockés dans <em>Vault</em>,</li>
<li>les modèles doivent être évités car nous avons <em>Jerikan</em> pour cela,</li>
<li>les tâches doivent être réutilisées et non dupliquées<sup id="fnref:duplication"><a class="footnote-ref" href="#fn:duplication">8</a></sup>.</li>
</ul>
<p>Nous évitons d’utiliser des collections d’<a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a>, à
l’exception de celles permettant de se connecter et d’interagir avec
les équipements constructeur, comme la <a href="https://galaxy.ansible.com/cisco/iosxr" title="Ansible Galaxy: cisco.iosxr collection">collection
<code>cisco.iosxr</code></a>. La qualité des collections d’<em>Ansible
Galaxy</em> est assez aléatoire et cela représente un effort de
maintenance supplémentaire. Nous préférons écrire des rôles adaptés à
nos besoins. Les collections que nous utilisons sont dans
<a href="https://github.com/jerikan-network/cmdb/blob/main/ci/ansible/ansible-galaxy.yaml"><code>ci/ansible/ansible-galaxy.yaml</code></a>. Nous
utilisons <a href="https://mitogen.networkgenomics.com/ansible_detailed.html" title="Mitogen for Ansible">Mitogen</a> pour accélérer considérablement les exécutions
d’<em>Ansible</em> sur les hôtes Linux.</p>
<p>Nous disposons également de quelques playbooks à des fins
opérationnelles : mise à niveau d’une version, isolation d’un routeur
Internet, etc. Nous avions également en tête d’ajouter une étape de
validation dans les rôles : est-ce que toutes les sessions BGP sont
opérationnelles ? Cela aurait permis de valider un déploiement et de
revenir rapidement en arrière en cas de problèmes.</p>
<p>Actuellement, nos playbooks sont exécutés à partir de nos ordinateurs
portables. Pour assurer le suivi des exécutions, nous utilisons
<a href="https://ara.recordsansible.org" title="ARA Records Ansible">ARA</a>. Un essai hebdomadaire sur les équipements du groupe <code>in-sync</code>
fournit également un tableau de bord sur les hôtes sur lesquels nous
devons faire converger la configuration.</p>
<h1 id="donnees-de-configuration-et-modeles">Données de configuration et modèles</h1>
<p>Nous livrons <em>Jerikan</em> avec des données et des modèles correspondant à
la configuration de nos centres de données USSFO03 et SK1. Ils
n’existent plus mais, nous vous le promettons, tout ceci était utilisé
en production à une époque ! 😢</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 900px"><span class="lf-media-inner" style="padding-bottom: 71.667%"><img alt="Architecture réseau pour les centres de données&#10;Blade" src="https://media-bernat-ch.b-cdn.net/images/jerikan/network@1x.a78efe4d6068fc.png" srcset="https://media-bernat-ch.b-cdn.net/images/jerikan/network@1x.a78efe4d6068fc.png 1131w,https://media-bernat-ch.b-cdn.net/images/jerikan/network@2x.be87506d03c707.png 1800w" sizes="auto, (max-width: 900px) 100vw, 900px" width="900" height="645" class="lf-media"/></span></div><figcaption>La dernière itération de notre infrastructure réseau pour SK1, USSFO03 et les futurs centres de données. Le réseau de production utilise BGPttH en utilisant une topologie Clos. Le réseau hors bande utilise une conception L2 simple, avec le protocole STP, ainsi qu'un ensemble de serveurs de console.</figcaption></figure>
<p>Vous trouverez la configuration pour :</p>
<dl>
<dt>Nos routeurs Internet</dt>
<dd>Certains fonctionnent sur <em>Junos</em>, comme <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/edge2.ussfo03.blade-group.net/config.txt">edge2.ussfo03</a>, les
  autres sur IOS XR, comme <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/edge1.sk1.blade-group.net/config.txt">edge1.sk1</a>. Les fonctionnalités
  implémentées sont similaires dans les deux cas et nous pouvons
  échanger l’un contre l’autre. Il y a la configuration BGP pour les
  transits, les peerings et les <abbr title="Internet Exchange">IX</abbr> ainsi que les politiques BGP
  associées. <a href="https://www.peeringdb.com/" title="The Interconnection Database">PeeringDB</a> est interrogé pour obtenir le nombre
  maximum de préfixes à accepter. <a href="/fr/blog/2020-bgpq4-irrd-docker" title="Accélérer bgpq4 avec IRRd dans un containeur">bgpq3 et un IRRd
  conteneurisé</a> aident à filtrer les
  routes reçues. Un pare-feu est également présent. IPv4 et IPv6 sont
  tous deux configurés.</dd>
<dt>Notre fabrique BGP</dt>
<dd>BGP est utilisé à l’intérieur du centre de données<sup id="fnref:evpn"><a class="footnote-ref" href="#fn:evpn">9</a></sup> et s’étend
  jusqu’aux hôtes physiques. La configuration est automatiquement
  dérivée de l’emplacement du dispositif et du numéro de
  port<sup id="fnref:bgptth"><a class="footnote-ref" href="#fn:bgptth">10</a></sup>. Les switchs en haut de chaque rack utilisent des
  sessions BGP passives pour les ports vers les serveurs. Ils exposent
  également un réseau spécifique pour permettre de démarrer en
  utilisant DHCP et PXE. Ils font également office de serveur DHCP. La
  conception est multivendeur : certains équipements fonctionnent sous
  <em>Cumulus Linux</em>, comme <a href="https://github.com/jerikan-network/cmdb/tree/generated-public/output/to1-p1.ussfo03.blade-group.net">to1-p1.ussfo03</a>, tandis que d’autres
  fonctionnent sous <em>Junos</em>, comme <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/to1-p2.ussfo03.blade-group.net/config.txt">to1-p2.ussfo03</a>.</dd>
<dt>Notre réseau hors bande</dt>
<dd>Nous utilisons des commutateurs Cisco Catalyst 2960 pour construire
  un réseau hors bande L2. Pour assurer la redondance et économiser
  quelques sous sur le câblage, nous construisons des petites boucles
  et exploitons le protocole <abbr title="Spanning Tree Protocol">STP</abbr>. Regardez <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/ob1-p1.ussfo03.blade-group.net/config.txt">ob1-p1.ussfo03</a>. Le
  réseau est connecté de manière redondante à des serveurs d’accès.
  Nous utilisons également des équipements OpenGear pour les accès
  console. Regardez <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/con1-n1.ussfo03.blade-group.net/config.txt">con1-n1.ussfo03</a>.</dd>
<dt>Nos passerelles d’accès</dt>
<dd>Ces serveurs Linux ont plusieurs objectifs : passerelles SSH,
  connexion de secours, accès direct au réseau hors bande,
  installation automatisée des équipements réseau<sup id="fnref:ztp"><a class="footnote-ref" href="#fn:ztp">11</a></sup>, accès
  Internet pour les flux administratifs, centralisation des serveurs
  de console à l’aide de <a href="https://conserver.com/">Conserver</a> et mise à disposition d’une API
  pour l’autoconfiguration des sessions BGP pour les serveurs
  physiques. Ce sont les premiers serveurs installés dans un nouveau
  centre de données et ils sont utilisés pour mettre en place tout le
  reste. Regardez à la fois les <a href="https://github.com/jerikan-network/cmdb/tree/generated-public/output/gateway1.ussfo03.blade-group.net">fichiers générés</a> et
  les <a href="https://github.com/jerikan-network/cmdb/tree/main/ansible/roles/blade.adm-gateway/tasks">tâches Ansible</a> associées.</dd>
</dl>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2021)</p>
<p>J’ai donné une présentation de Jerikan et Ansible durant le
<a href="/fr/blog/2021-frnog34-jerikan">FRnOG #34</a>. Elle reprend
essentiellement le contenu de cet article.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>Cet article a été brièvement discuté sur <a href="https://news.ycombinator.com/item?id=27275995">Hacker News</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:debug">
<p><em>Ansible</em> ne fournit même pas un numéro de ligne lorsqu’il y
a une erreur dans un modèle. Il faut parfois trouver le problème
par bissection.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>ansible<span class="w"> </span>--version
<span class="go">ansible 2.10.8</span>
<span class="go">[…]</span>
<span class="gp">$ </span>cat<span class="w"> </span>test.j2
<span class="go">Hello {{ name }}!</span>
<span class="gp">$ </span>ansible<span class="w"> </span>all<span class="w"> </span>-i<span class="w"> </span>localhost,<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w"> </span>--connection<span class="o">=</span><span class="nb">local</span><span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w"> </span>-m<span class="w"> </span>template<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w"> </span>-a<span class="w"> </span><span class="s2">"src=test.j2 dest=test.txt"</span>
<span class="go">localhost | FAILED! =&gt; {</span>
<span class="go">    "changed": false,</span>
<span class="go">    "msg": "AnsibleUndefinedVariable: 'name' is undefined"</span>
<span class="go">}</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:debug" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:jerakia">
<p>Ce sont les mêmes concepts que dans <a href="https://help.puppet.com/core/current/Content/PuppetCore/hiera_intro.htm">Hiera</a>,
le système clés-valeurs hiérarchique de <em>Puppet</em>. Au départ, nous
utilisions <a href="https://web.archive.org/web/20210520014417/http://jerakia.io/" title="Jerakia: a versatile data lookup system">Jerakia</a>, un système similaire indépendant qui
expose une interface HTTP REST. Cependant, le surcoût en temps
d’une recherche est trop important pour notre utilisation.
<em>Jerikan</em> implémente la même fonctionnalité dans une <a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jerakia.py">fonction
Python</a>. <a class="footnote-backref" href="#fnref:jerakia" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:schema">
<p>Ce fichier pourrait également être utilisé pour définir un
schéma validant la source de vérité et détecter les erreurs plus
tôt. Il pourrait aussi indiquer des valeurs par défaut pour éviter
de déléguer cette tâche aux modèles. <a href="https://github.com/23andMe/Yamale" title="A schema and validator for YAML">Yamale</a> pourrait être une
première brique pour cette fonctionnalité. <a class="footnote-backref" href="#fnref:schema" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:extend">
<p>La liste des filtres est placée en dur dans
<a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jinja.py"><code>jerikan/jinja.py</code></a>. C’est une conséquence du
fait que nous ne maintenons pas <em>Jerikan</em> comme un logiciel
indépendant. <a class="footnote-backref" href="#fnref:extend" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:function">
<p>C’est un peu confus. Nous avons un filtre <code>store()</code> et
une fonction <code>store()</code>. Dans <em>Jinja2</em>, les filtres et les
fonctions vivent dans deux espaces de noms distincts. <a class="footnote-backref" href="#fnref:function" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:fork">
<p>Nous utilisons une <a href="https://github.com/jerikan-network/junoser" title="Junoser fork">version modifiée</a> pour pouvoir
valider nos configurations et exposer un service HTTP pour réduire
le temps de validation. <a class="footnote-backref" href="#fnref:fork" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:derivation">
<p>Il y a une tendance dans l’automatisation des réseaux à
se limiter à un sous-ensemble de configuration, par exemple en
ayant un playbook pour créer une nouvelle session BGP. Nous
pensons que c’est une mauvaise approche : avec le temps, votre
configuration ne correspondra pas à l’état attendu, notamment les
changements faits à la main ne seront pas détectés. <a class="footnote-backref" href="#fnref:derivation" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:duplication">
<p>Voir
<a href="https://github.com/jerikan-network/cmdb/blob/main/ansible/roles/blade.linux/tasks/firewall.yaml"><code>ansible/roles/blade.linux/tasks/firewall.yaml</code></a>
et
<a href="https://github.com/jerikan-network/cmdb/blob/main/ansible/roles/blade.linux/tasks/interfaces.yaml"><code>ansible/roles/blade.linux/tasks/interfaces.yaml</code></a>.
Ils sont destinés à être appelés lorsque cela est nécessaire, en
utilisant <code>import_role</code>. <a class="footnote-backref" href="#fnref:duplication" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:evpn">
<p>Nous avons également quelques centres de données qui
utilisent BGP EVPN VXLAN avec des équipements <em>Juniper</em>. Comme ils
sont encore en production aujourd’hui, nous n’avons pas inclus
cette fonctionnalité mais nous pourrions la publier à l’avenir. <a class="footnote-backref" href="#fnref:evpn" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:bgptth">
<p>Rétrospectivement, ce n’est peut-être pas une bonne idée,
sauf si vous êtes à peu près sûr que tout est immuable. Ce n’était
pas notre cas. Nous pensons maintenant qu’il aurait été plus
judicieux d’attribuer un préfixe à chaque appareil et de le
consigner dans la source de vérité. <a class="footnote-backref" href="#fnref:bgptth" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:ztp">
<p>Les équipements n’utilisant pas Linux sont mis à jour et
configurés sans interaction de notre part. Les périphériques
<em>Cumulus Linux</em> sont automatiquement mis à jour lors de leur
première installation mais la configuration finale doit être
poussée en utilisant <em>Ansible</em> : nous ne voulions pas dupliquer le
processus de configuration en utilisant un autre outil. <a class="footnote-backref" href="#fnref:ztp" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Synchroniser des objets RIPE, ARIN et APNIC avec un module Ansible sur mesure</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2020-synchro-ripe-arin-apnic-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2020-synchro-ripe-arin-apnic-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2020-09-28T06:33:30Z</updated>
    <id>http://www.luffy.cx/fr/blog/2020-synchro-ripe-arin-apnic-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>L’Internet est divisé en cinq <a href="https://en.wikipedia.org/wiki/Regional_Internet_registry" title="Regional Internet registry on Wikipedia">registres Internet régionaux</a> : <abbr title="African Network Information Center">AFRINIC</abbr>, <abbr title="American Registry for Internet Numbers">ARIN</abbr>, <abbr title="Asia-Pacific Network Information Centre">APNIC</abbr>, <abbr title="Latin America and Caribbean Network Information Centre">LACNIC</abbr> et <abbr title="Réseaux IP Européens Network Coordination Centre">RIPE</abbr>. Chacun de
ces <abbr title="Regional Internet Registry">RIR</abbr> gère un <a href="https://en.wikipedia.org/wiki/Internet_Routing_Registry" title="Internet Routing Registry on Wikipedia">registre de routage Internet</a>. Un tel <abbr title="Internet Routing Registry">IRR</abbr> permet de publier des <strong>informations sur le
routage des ressources Internet</strong><sup id="fnref:others"><a class="footnote-ref" href="#fn:others">1</a></sup>. Les opérateurs l’utilisent
pour déterminer le propriétaire d’une adresse IP et pour construire et
maintenir des <strong>filtres de routage</strong>. Pour que vos routes soient
largement acceptées, il est important de tenir à jour les préfixes que
vous annoncez dans un <abbr title="Internet Routing Registry">IRR</abbr>.</p>
<p>Il existe deux outils courants pour interroger cette base de données :
<code>whois</code> et <code>bgpq4</code>. Le premier vous permet d’effectuer une requête
avec le <a href="https://www.rfc-editor.org/rfc/rfc3912" title="RFC 3912: WHOIS Protocol Specification">protocole WHOIS</a> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>whois<span class="w"> </span>-h<span class="w"> </span>whois.ripe.net<span class="w"> </span>--<span class="w"> </span><span class="s1">'-BrG 2a0a:e805:400::/40'</span>
<span class="go">[…]</span>
<span class="go">inet6num:       2a0a:e805:400::/40</span>
<span class="go">netname:        FR-BLADE-CUSTOMERS-DE</span>
<span class="go">country:        DE</span>
<span class="go">geoloc:         50.1109 8.6821</span>
<span class="go">admin-c:        BN2763-RIPE</span>
<span class="go">tech-c:         BN2763-RIPE</span>
<span class="go">status:         ASSIGNED</span>
<span class="go">mnt-by:         fr-blade-1-mnt</span>
<span class="go">remarks:        synced with cmdb</span>
<span class="go">created:        2020-05-19T08:04:58Z</span>
<span class="go">last-modified:  2020-05-19T08:04:58Z</span>
<span class="go">source:         RIPE</span>

<span class="go">route6:         2a0a:e805:400::/40</span>
<span class="go">descr:          Blade IPv6 - AMS1</span>
<span class="go">origin:         AS64476</span>
<span class="go">mnt-by:         fr-blade-1-mnt</span>
<span class="go">remarks:        synced with cmdb</span>
<span class="go">created:        2019-10-01T08:19:34Z</span>
<span class="go">last-modified:  2020-05-19T08:05:00Z</span>
<span class="go">source:         RIPE</span>
</code></pre></div>

<p>Le second permet de construire des filtres de routage en utilisant les
informations contenues dans la base de données <abbr title="Internet Routing Registry">IRR</abbr> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>bgpq4<span class="w"> </span>-6<span class="w"> </span>-S<span class="w"> </span>RIPE<span class="w"> </span>-b<span class="w"> </span>AS64476
<span class="go">NN = [</span>
<span class="go">    2a0a:e805::/40,</span>
<span class="go">    2a0a:e805:100::/40,</span>
<span class="go">    2a0a:e805:300::/40,</span>
<span class="go">    2a0a:e805:400::/40,</span>
<span class="go">    2a0a:e805:500::/40</span>
<span class="go">];</span>
</code></pre></div>

<p>Il n’y a pas de module disponible sur <a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a> pour gérer
ces objets. Chaque <abbr title="Internet Routing Registry">IRR</abbr> a différentes façons d’être mis à jour.
Certains <abbr title="Regional Internet Registry">RIR</abbr> proposent une API, mais d’autres non. Si l’on se limite
au <abbr title="Réseaux IP Européens Network Coordination Centre">RIPE</abbr>, à l’<abbr title="American Registry for Internet Numbers">ARIN</abbr> et à l’<abbr title="Asia-Pacific Network Information Centre">APNIC</abbr>, la seule méthode commune de mise à
jour des objets est la <a href="https://docs.db.ripe.net/Update-Methods/Email-Updates" title="RIPE NCC: Email Updates">mise à jour par courrier électronique</a>, authentifiée par un mot de passe ou une signature PGP<sup id="fnref:support"><a class="footnote-ref" href="#fn:support">2</a></sup>.
Écrivons un module Ansible à cet effet !</p>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Je vous recommande de lire « <a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">Écrire un module Ansible sur mesure</a> » en
introduction, ainsi que « <a href="/fr/blog/2020-synchro-tables-mysql-ansible" title="Synchroniser des tables MySQL avec un module Ansible sur mesure">Synchroniser des tables MySQL</a> » pour un
premier exemple plus didactique.</p>
</div>
<div class="toc">
<ul>
<li><a href="#code">Code</a><ul>
<li><a href="#prerequis">Prérequis</a></li>
<li><a href="#definition-du-module">Définition du module</a></li>
<li><a href="#obtenir-les-objets-existants">Obtenir les objets existants</a></li>
<li><a href="#comparer-avec-les-objets-souhaites">Comparer avec les objets souhaités</a></li>
<li><a href="#retourner-les-mises-a-jour">Retourner les mises à jour</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="code">Code</h1>
<p>Le <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">module</a> prend une liste d’objets <abbr title="Routing Policy Specification Language">RPSL</abbr> à synchroniser et renvoie
le contenu d’un courrier électronique pour mise à jour si un changement
est nécessaire :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">prepare RIPE objects</span>
<span class="w">  </span><span class="nt">irr_sync</span><span class="p">:</span>
<span class="w">    </span><span class="nt">irr</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">RIPE</span>
<span class="w">    </span><span class="nt">mntner</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">fr-blade-1-mnt</span>
<span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">whois-ripe.txt</span>
<span class="w">  </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">irr</span>
</code></pre></div>

<h2 id="prerequis">Prérequis</h2>
<p>Le fichier source doit être un ensemble d’objets à synchroniser en
utilisant le <a href="https://www.rfc-editor.org/rfc/rfc2622" title="RFC 2622: Routing Policy Specification Language (RPSL)">langage <abbr title="Routing Policy Specification Language">RPSL</abbr></a>. Il s’agirait du même
contenu que celui que vous enverriez manuellement par courrier
électronique. Tous les objets doivent être gérés par le même
mainteneur, également fourni en paramètre.</p>
<p>La signature<sup id="fnref:arin"><a class="footnote-ref" href="#fn:arin">3</a></sup> et l’envoi du résultat ne relèvent pas de la
responsabilité de ce module. Vous avez besoin de deux tâches
supplémentaires à cette fin :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">sign RIPE objects</span>
<span class="w">  </span><span class="nt">shell</span><span class="p">:</span>
<span class="w">    </span><span class="nt">cmd</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">gpg --batch --local-user noc@example.com --clearsign</span>
<span class="hll"><span class="w">    </span><span class="nt">stdin</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">irr.objects</span><span class="nv"> </span><span class="s">}}"</span>
</span><span class="hll"><span class="w">  </span><span class="nt">register</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">signed</span>
</span><span class="w">  </span><span class="nt">check_mode</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>
<span class="w">  </span><span class="nt">changed_when</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>

<span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">update RIPE objects by email</span>
<span class="w">  </span><span class="nt">mail</span><span class="p">:</span>
<span class="w">    </span><span class="nt">subject</span><span class="p">:</span><span class="w"> </span><span class="s">"NEW:</span><span class="nv"> </span><span class="s">update</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">RIPE"</span>
<span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">noc@example.com</span>
<span class="w">    </span><span class="nt">to</span><span class="p">:</span><span class="w"> </span><span class="s">"auto-dbm@ripe.net"</span>
<span class="w">    </span><span class="nt">cc</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">noc@example.com</span>
<span class="w">    </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">smtp.example.com</span>
<span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">25</span>
<span class="w">    </span><span class="nt">charset</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">us-ascii</span>
<span class="hll"><span class="w">    </span><span class="nt">body</span><span class="p">:</span><span class="w"> </span><span class="s">"{{</span><span class="nv"> </span><span class="s">signed.stdout</span><span class="nv"> </span><span class="s">}}"</span>
</span></code></pre></div>

<p>Vous devez aussi autoriser la clé PGP utilisée pour signer les mises
à jour en créant un objet <code>key-cert</code> et en l’ajoutant comme méthode
d’authentification pour l’objet <code>mntner</code> correspondant :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">key-cert</span><span class="p">:</span><span class="w">  </span><span class="l l-Scalar l-Scalar-Plain">PGPKEY-A791AAAB</span>
<span class="nt">certif</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">-----BEGIN PGP PUBLIC KEY BLOCK-----</span>
<span class="nt">certif</span><span class="p">:</span>
<span class="nt">certif</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">mQGNBF8TLY8BDADEwP3a6/vRhEERBIaPUAFnr23zKCNt5YhWRZyt50mKq1RmQBBY</span>
<span class="p p-Indicator">[</span><span class="nv">…</span><span class="p p-Indicator">]</span>
<span class="nt">certif</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">-----END PGP PUBLIC KEY BLOCK-----</span>
<span class="nt">mnt-by</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">fr-blade-1-mnt</span>
<span class="nt">source</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">RIPE</span>

<span class="nt">mntner</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">fr-blade-1-mnt</span>
<span class="p p-Indicator">[</span><span class="nv">…</span><span class="p p-Indicator">]</span>
<span class="nt">auth</span><span class="p">:</span><span class="w">      </span><span class="l l-Scalar l-Scalar-Plain">PGPKEY-A791AAAB</span>
<span class="nt">mnt-by</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">fr-blade-1-mnt</span>
<span class="nt">source</span><span class="p">:</span><span class="w">    </span><span class="l l-Scalar l-Scalar-Plain">RIPE</span>
</code></pre></div>

<h2 id="definition-du-module">Définition du module</h2>
<p>En se basant sur le squelette présenté dans l’<a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">article
précédent</a>, nous définissons le
module :</p>
<div class="language-python codehilite"><pre><span/><code><span class="n">module_args</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="n">irr</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'str'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
    <span class="n">mntner</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'str'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
    <span class="n">source</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'path'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
<span class="p">)</span>

<span class="n">result</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="n">changed</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>

<span class="n">module</span> <span class="o">=</span> <span class="n">AnsibleModule</span><span class="p">(</span>
    <span class="n">argument_spec</span><span class="o">=</span><span class="n">module_args</span><span class="p">,</span>
    <span class="n">supports_check_mode</span><span class="o">=</span><span class="kc">True</span>
<span class="p">)</span>
</code></pre></div>

<h2 id="obtenir-les-objets-existants">Obtenir les objets existants</h2>
<p>Pour récupérer les objets existants, la commande <code>whois</code> recherche
tous les objets attachés au mainteneur fourni en paramètre du module.</p>
<div class="language-python codehilite"><pre><span/><code><span class="c1"># Per-IRR variations:</span>
<span class="c1"># - whois server</span>
<span class="n">whois</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s1">'ARIN'</span><span class="p">:</span> <span class="s1">'rr.arin.net'</span><span class="p">,</span>
    <span class="s1">'RIPE'</span><span class="p">:</span> <span class="s1">'whois.ripe.net'</span><span class="p">,</span>
    <span class="s1">'APNIC'</span><span class="p">:</span> <span class="s1">'whois.apnic.net'</span>
<span class="p">}</span>
<span class="c1"># - whois options</span>
<span class="n">options</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s1">'ARIN'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'-r'</span><span class="p">],</span>
    <span class="s1">'RIPE'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'-BrG'</span><span class="p">],</span>
    <span class="s1">'APNIC'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'-BrG'</span><span class="p">]</span>
<span class="p">}</span>
<span class="c1"># - objects excluded from synchronization</span>
<span class="n">excluded</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"domain"</span><span class="p">]</span>
<span class="k">if</span> <span class="n">irr</span> <span class="o">==</span> <span class="s2">"ARIN"</span><span class="p">:</span>
    <span class="c1"># ARIN does not return these objects</span>
    <span class="n">excluded</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span>
        <span class="s2">"key-cert"</span><span class="p">,</span>
        <span class="s2">"mntner"</span><span class="p">,</span>
    <span class="p">])</span>

<span class="c1"># Grab existing objects</span>
<span class="n">args</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-h"</span><span class="p">,</span> <span class="n">whois</span><span class="p">[</span><span class="n">irr</span><span class="p">],</span>
        <span class="s2">"-s"</span><span class="p">,</span> <span class="n">irr</span><span class="p">,</span>
        <span class="o">*</span><span class="n">options</span><span class="p">[</span><span class="n">irr</span><span class="p">],</span>
        <span class="s2">"-i"</span><span class="p">,</span> <span class="s2">"mnt-by"</span><span class="p">,</span>
        <span class="n">module</span><span class="o">.</span><span class="n">params</span><span class="p">[</span><span class="s1">'mntner'</span><span class="p">]]</span>
<span class="n">proc</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">"whois"</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">if</span> <span class="n">proc</span><span class="o">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
    <span class="k">raise</span> <span class="n">AnsibleError</span><span class="p">(</span>
        <span class="sa">f</span><span class="s2">"unable to query whois: </span><span class="si">{</span><span class="n">args</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">proc</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s1">'ascii'</span><span class="p">)</span>
<span class="n">got</span> <span class="o">=</span> <span class="n">extract</span><span class="p">(</span><span class="n">output</span><span class="p">,</span> <span class="n">excluded</span><span class="p">)</span>
</code></pre></div>

<p>La première partie du code introduit des constantes spécifiques à
chaque <abbr title="Internet Routing Registry">IRR</abbr> : le serveur à interroger, les options à fournir à la
commande <code>whois</code> et les objets à exclure de la synchronisation. La
seconde partie invoque la commande <code>whois</code> qui récupère tous les
objets dont le champ <code>mnt-by</code> correspond au mainteneur fourni. Voici
un exemple de sortie :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>whois<span class="w"> </span>-h<span class="w"> </span>whois.ripe.net<span class="w"> </span>--<span class="w"> </span><span class="s1">'-s RIPE -BrG -i mnt-by fr-blade-1-mnt'</span>
<span class="go">[…]</span>

<span class="go">inet6num:       2a0a:e805:300::/40</span>
<span class="go">netname:        FR-BLADE-CUSTOMERS-FR</span>
<span class="go">country:        FR</span>
<span class="go">geoloc:         48.8566 2.3522</span>
<span class="go">admin-c:        BN2763-RIPE</span>
<span class="go">tech-c:         BN2763-RIPE</span>
<span class="go">status:         ASSIGNED</span>
<span class="go">mnt-by:         fr-blade-1-mnt</span>
<span class="go">remarks:        synced with cmdb</span>
<span class="go">created:        2020-05-19T08:04:59Z</span>
<span class="go">last-modified:  2020-05-19T08:04:59Z</span>
<span class="go">source:         RIPE</span>

<span class="go">[…]</span>

<span class="go">route6:         2a0a:e805:300::/40</span>
<span class="go">descr:          Blade IPv6 - PA1</span>
<span class="go">origin:         AS64476</span>
<span class="go">mnt-by:         fr-blade-1-mnt</span>
<span class="go">remarks:        synced with cmdb</span>
<span class="go">created:        2019-10-01T08:19:34Z</span>
<span class="go">last-modified:  2020-05-19T08:05:00Z</span>
<span class="go">source:         RIPE</span>

<span class="go">[…]</span>
</code></pre></div>

<p>Le résultat est transmis à la fonction <code>extract()</code>. Elle analyse et
normalise les résultats dans un dictionnaire qui met en correspondance
les noms d’objets et les objets. Nous stockons le résultat dans la
variable <code>got</code>.</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">def</span><span class="w"> </span><span class="nf">extract</span><span class="p">(</span><span class="n">raw</span><span class="p">,</span> <span class="n">excluded</span><span class="p">):</span>
<span class="w">    </span><span class="sd">"""Extract objects."""</span>
    <span class="c1"># First step, remove comments and unwanted lines</span>
    <span class="n">objects</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">obj</span>
                         <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">raw</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
                         <span class="k">if</span> <span class="ow">not</span> <span class="n">obj</span><span class="o">.</span><span class="n">startswith</span><span class="p">((</span>
                                 <span class="s2">"#"</span><span class="p">,</span>
                                 <span class="s2">"%"</span><span class="p">,</span>
                         <span class="p">))])</span>
    <span class="c1"># Second step, split objects</span>
    <span class="n">objects</span> <span class="o">=</span> <span class="p">[</span><span class="n">RPSLObject</span><span class="p">(</span><span class="n">obj</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span>
               <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s2">"\n\n+"</span><span class="p">,</span> <span class="n">objects</span><span class="p">)</span>
               <span class="k">if</span> <span class="n">obj</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
               <span class="ow">and</span> <span class="ow">not</span> <span class="n">obj</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span>
                   <span class="nb">tuple</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">x</span><span class="si">}</span><span class="s2">:"</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">excluded</span><span class="p">))]</span>
    <span class="c1"># Last step, put objects in a dict</span>
    <span class="n">objects</span> <span class="o">=</span> <span class="p">{</span><span class="nb">repr</span><span class="p">(</span><span class="n">obj</span><span class="p">):</span> <span class="n">obj</span>
               <span class="k">for</span> <span class="n">obj</span> <span class="ow">in</span> <span class="n">objects</span><span class="p">}</span>
    <span class="k">return</span> <span class="n">objects</span>
</code></pre></div>

<p><code>RPSLObject()</code> est la classe qui permet de normaliser et comparer les
objets. Regardez le <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">code du module</a> pour plus de détails.</p>
<div class="language-python-console-session codehilite"><pre><span/><code><span class="gp">&gt;&gt;&gt; </span><span class="n">output</span><span class="o">=</span><span class="s2">"""</span>
<span class="gp">... </span><span class="s2">inet6num:       2a0a:e805:300::/40</span>
<span class="gp">... </span><span class="s2">[…]</span>
<span class="gp">... </span><span class="s2">"""</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">pprint</span><span class="p">({</span><span class="n">k</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span><span class="n">v</span> <span class="ow">in</span> <span class="n">extract</span><span class="p">(</span><span class="n">output</span><span class="p">,</span> <span class="n">excluded</span><span class="o">=</span><span class="p">[])})</span>
<span class="go">{'&lt;Object:inet6num:2a0a:e805:300::/40&gt;':</span>
<span class="go">   'inet6num:       2a0a:e805:300::/40\n'</span>
<span class="go">   'netname:        FR-BLADE-CUSTOMERS-FR\n'</span>
<span class="go">   'country:        FR\n'</span>
<span class="go">   'geoloc:         48.8566 2.3522\n'</span>
<span class="go">   'admin-c:        BN2763-RIPE\n'</span>
<span class="go">   'tech-c:         BN2763-RIPE\n'</span>
<span class="go">   'status:         ASSIGNED\n'</span>
<span class="go">   'mnt-by:         fr-blade-1-mnt\n'</span>
<span class="go">   'remarks:        synced with cmdb\n'</span>
<span class="go">   'source:         RIPE',</span>
<span class="go"> '&lt;Object:route6:2a0a:e805:300::/40&gt;':</span>
<span class="go">   'route6:         2a0a:e805:300::/40\n'</span>
<span class="go">   'descr:          Blade IPv6 - PA1\n'</span>
<span class="go">   'origin:         AS64476\n'</span>
<span class="go">   'mnt-by:         fr-blade-1-mnt\n'</span>
<span class="go">   'remarks:        synced with cmdb\n'</span>
<span class="go">   'source:         RIPE'}</span>
</code></pre></div>

<h2 id="comparer-avec-les-objets-souhaites">Comparer avec les objets souhaités</h2>
<p>Construisons maintenant le dictionnaire <code>wanted</code> en réutilisant la
fonction <code>extract()</code> :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">params</span><span class="p">[</span><span class="s1">'source'</span><span class="p">])</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">source</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
<span class="n">wanted</span> <span class="o">=</span> <span class="n">extract</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">excluded</span><span class="p">)</span>
</code></pre></div>

<p>L’étape suivante est de comparer <code>got</code> et <code>wanted</code> pour construire les
différences :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">if</span> <span class="n">got</span> <span class="o">!=</span> <span class="n">wanted</span><span class="p">:</span>
    <span class="n">result</span><span class="p">[</span><span class="s1">'changed'</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
    <span class="k">if</span> <span class="n">module</span><span class="o">.</span><span class="n">_diff</span><span class="p">:</span>
        <span class="n">result</span><span class="p">[</span><span class="s1">'diff'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="nb">dict</span><span class="p">(</span><span class="n">before_header</span><span class="o">=</span><span class="n">k</span><span class="p">,</span>
                 <span class="n">after_header</span><span class="o">=</span><span class="n">k</span><span class="p">,</span>
                 <span class="n">before</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">got</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="s2">""</span><span class="p">)),</span>
                 <span class="n">after</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">wanted</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="s2">""</span><span class="p">)))</span>
            <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">set</span><span class="p">((</span><span class="o">*</span><span class="n">wanted</span><span class="o">.</span><span class="n">keys</span><span class="p">(),</span> <span class="o">*</span><span class="n">got</span><span class="o">.</span><span class="n">keys</span><span class="p">()))</span>
            <span class="k">if</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">wanted</span> <span class="ow">or</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">got</span> <span class="ow">or</span> <span class="n">wanted</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">!=</span> <span class="n">got</span><span class="p">[</span><span class="n">k</span><span class="p">]]</span>
</code></pre></div>

<h2 id="retourner-les-mises-a-jour">Retourner les mises à jour</h2>
<p>Le module n’a pas d’effet de bord. S’il y a une différence, nous
retournons les mises à jour à envoyer par courrier électronique. Nous
choisissons d’inclure tous les objets souhaités dans les mises à jour
(contenus dans la variable <code>source</code>) et de laisser l’<abbr title="Internet Routing Registry">IRR</abbr> ignorer les
objets non modifiés. Nous ajoutons également les objets à supprimer en
ajoutant un attribut <code>delete:</code> à chacun d’entre eux.</p>
<div class="language-python codehilite"><pre><span/><code><span class="c1"># We send all source objects and deleted objects.</span>
<span class="n">deleted_mark</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="s1">'delete:'</span><span class="si">:</span><span class="s2">16</span><span class="si">}</span><span class="s2">deleted by CMDB"</span>
<span class="n">deleted</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\n\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">got</span><span class="p">[</span><span class="n">k</span><span class="p">]</span><span class="o">.</span><span class="n">raw</span><span class="si">}</span><span class="se">\n</span><span class="si">{</span><span class="n">deleted_mark</span><span class="si">}</span><span class="s2">"</span>
                       <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">got</span>
                       <span class="k">if</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">wanted</span><span class="p">])</span>
<span class="n">result</span><span class="p">[</span><span class="s1">'objects'</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">source</span><span class="si">}</span><span class="se">\n\n</span><span class="si">{</span><span class="n">deleted</span><span class="si">}</span><span class="s2">"</span>

<span class="n">module</span><span class="o">.</span><span class="n">exit_json</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>
</code></pre></div>

<hr/>
<p>Le code complet est disponible sur <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">GitHub</a>. Le module prend
en charge les options <code>--diff</code> et <code>--check</code>. Il ne renvoie rien si
aucun changement n’est détecté. Il peut fonctionner avec l’<abbr title="Asia-Pacific Network Information Centre">APNIC</abbr>, le
<abbr title="Réseaux IP Européens Network Coordination Centre">RIPE</abbr> et l’<abbr title="American Registry for Internet Numbers">ARIN</abbr>. Il n’est pas parfait : il peut ne pas détecter
certains changements<sup id="fnref:changes"><a class="footnote-ref" href="#fn:changes">4</a></sup>, il n’est pas capable de modifier des
objets n’appartenant pas au mainteneur fourni<sup id="fnref:mntner"><a class="footnote-ref" href="#fn:mntner">5</a></sup> et certains
attributs refusent les mises à jour, ce qui nécessite de supprimer et
de recréer manuellement l’objet<sup id="fnref:modify"><a class="footnote-ref" href="#fn:modify">6</a></sup>. Cependant, ce module devrait
automatiser 95% de vos besoins.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:others">
<p>D’autres <abbr title="Internet Routing Registry">IRR</abbr> existent en dehors de ceux maintenus par les
<abbr title="Regional Internet Registry">RIR</abbr>. Le plus connu est <a href="https://www.radb.net/" title="RADb">RADb</a>. <a class="footnote-backref" href="#fnref:others" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:support">
<p>L’<abbr title="American Registry for Internet Numbers">ARIN</abbr> abandonne progressivement cette méthode au profit de
<a href="https://www.arin.net/resources/manage/irr/irr-online-implementation/#using-the-web-based-interface-irr-online" title="ARIN: Using the Web-Based Interface (IRR-online)"><abbr title="Internet Routing Registry">IRR</abbr>-online</a>. De plus, les signatures PGP n’ont jamais été
supportées en dehors de l’environnement de test. <abbr title="Réseaux IP Européens Network Coordination Centre">RIPE</abbr> dispose
d’une <a href="https://docs.db.ripe.net/Update-Methods/RESTful-API" title="RIPE Database RESTful API">API</a>, mais les mises à jour par courrier
électronique sont toujours prises en charge et il n’est pas prévu
de les faire disparaître. L’<abbr title="Asia-Pacific Network Information Centre">APNIC</abbr> prévoit d’exposer une
<a href="https://www.apnic.net/manage-ip/apnic-services/services-roadmap/public-api-draft-for-members/" title="APNIC: Public API Draft (for Members)">API</a>. <a class="footnote-backref" href="#fnref:support" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:arin">
<p>PGP n’est pas pris en charge par l’<abbr title="American Registry for Internet Numbers">ARIN</abbr> en dehors de
l’environnement de test. Seule l’authentification par mot de passe
est disponible… 😕 <a class="footnote-backref" href="#fnref:arin" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:changes">
<p>Pour l’<abbr title="American Registry for Internet Numbers">ARIN</abbr>, nous ne pouvons pas interroger les objets
<code>key-cert</code> et <code>mntner</code> et donc nous ne pouvons pas détecter les
changements qui s’y produisent. Il n’est pas non plus possible de
détecter les modifications des mécanismes d’authentification d’un
objet <code>mntner</code>. <a class="footnote-backref" href="#fnref:changes" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:mntner">
<p>L’<abbr title="Asia-Pacific Network Information Centre">APNIC</abbr> n’attribue pas les objets de plus haut niveau au
mainteneur associé au propriétaire. <a class="footnote-backref" href="#fnref:mntner" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:modify">
<p>Pour modifier le statut d’un objet <code>inetnum</code>, il faut
supprimer et recréer l’objet. <a class="footnote-backref" href="#fnref:modify" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Synchroniser NetBox avec un module Ansible sur mesure</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2020-synchro-netbox-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2020-synchro-netbox-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2020-09-19T14:11:34Z</updated>
    <id>http://www.luffy.cx/fr/blog/2020-synchro-netbox-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>La <a href="https://netbox-ansible-collection.readthedocs.io/en/latest/" title="netbox.netbox Ansible Collection's documentation">collection <code>netbox.netbox</code></a> d’<a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a>
fournit divers modules pour mettre à jour des objets dans <a href="https://netbox.readthedocs.io/en/stable/" title="NetBox documentation">NetBox</a>:</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">create a device in NetBox</span>
<span class="w">  </span><span class="nt">netbox_device</span><span class="p">:</span>
<span class="w">    </span><span class="nt">netbox_url</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">http://netbox.local</span>
<span class="w">    </span><span class="nt">netbox_token</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">s3cret</span>
<span class="w">    </span><span class="nt">data</span><span class="p">:</span>
<span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">to3-p14.sfo1.example.com</span>
<span class="w">      </span><span class="nt">device_type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">QFX5110-48S</span>
<span class="w">      </span><span class="nt">device_role</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Compute Switch</span>
<span class="w">      </span><span class="nt">site</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">SFO1</span>
</code></pre></div>

<p>Cependant, si <em>NetBox</em> n’est <strong>pas votre source de vérité</strong>, vous
voudrez vous assurer qu’il reste synchronisé avec votre base de
gestion de configuration<sup id="fnref:cmdb"><a class="footnote-ref" href="#fn:cmdb">1</a></sup> en supprimant les équipements ou les
adresses IP obsolètes. Alors qu’il devrait être possible de bricoler
un <em>playbook</em> avec une requête, une boucle et des filtres pour
supprimer les éléments indésirables, cela semble fastidieux,
inefficace et constitue un détournement de YAML comme langage de
programmation. Un module Ansible ad hoc résout ce problème et est
probablement plus flexible.</p>
<div class="toc">
<ul>
<li><a href="#code">Code</a><ul>
<li><a href="#definition-du-module">Définition du module</a></li>
<li><a href="#abstraction-du-processus-de-synchronisation">Abstraction du processus de synchronisation</a><ul>
<li><a href="#synchroniser-les-etiquettes-et-les-tenants">Synchroniser les étiquettes et les tenants</a></li>
<li><a href="#synchroniser-les-sites">Synchroniser les sites</a></li>
<li><a href="#synchroniser-les-constructeurs-les-types-dequipements-et-les-roles-des-equipements">Synchroniser les constructeurs, les types d’équipements et les rôles des équipements</a></li>
<li><a href="#synchroniser-les-equipements">Synchroniser les équipements</a></li>
<li><a href="#synchroniser-les-adresses-ip">Synchroniser les adresses IP</a></li>
</ul>
</li>
<li><a href="#calculer-les-etats-actuels-et-attendus">Calculer les états actuels et attendus</a></li>
<li><a href="#appliquer-les-changements">Appliquer les changements</a></li>
</ul>
</li>
</ul>
</div>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Je vous recommande de lire « <a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">Écrire un module Ansible sur mesure</a> » en
introduction, ainsi que « <a href="/fr/blog/2020-synchro-tables-mysql-ansible" title="Synchroniser des tables MySQL avec un module Ansible sur mesure">Synchroniser des tables MySQL</a> » pour un
premier exemple plus didactique.</p>
</div>
<h1 id="code">Code</h1>
<p>Le <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">module</a> expose la signature suivante et synchronise <em>NetBox</em>
avec le contenu du fichier YAML fourni :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">netbox_sync</span><span class="p">:</span>
<span class="w">  </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">netbox.yaml</span>
<span class="w">  </span><span class="nt">api</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://netbox.example.com</span>
<span class="w">  </span><span class="nt">token</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">s3cret</span>
</code></pre></div>

<p>Les objets synchronisés sont :</p>
<ul>
<li>les sites ;</li>
<li>les constructeurs ;</li>
<li>les types d’équipements ;</li>
<li>les rôles des équipements ;</li>
<li>les équipements ;</li>
<li>les adresses IP.</li>
</ul>
<p>Dans notre environnement, le fichier YAML est généré à partir de notre
source de vérité et contient un ensemble d’équipements et une liste
d’adresses IP :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">devices</span><span class="p">:</span>
<span class="w">  </span><span class="nt">ad2-p6.sfo1.example.com</span><span class="p">:</span>
<span class="w">     </span><span class="nt">datacenter</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">sfo1</span>
<span class="w">     </span><span class="nt">manufacturer</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Cisco</span>
<span class="w">     </span><span class="nt">model</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Catalyst 2960G-48TC-L</span>
<span class="w">     </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">net_tor_oob_switch</span>
<span class="w">  </span><span class="nt">to1-p6.sfo1.example.com</span><span class="p">:</span>
<span class="w">     </span><span class="nt">datacenter</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">sfo1</span>
<span class="w">     </span><span class="nt">manufacturer</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Juniper</span>
<span class="w">     </span><span class="nt">model</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">QFX5110-48S</span>
<span class="w">     </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">net_tor_gpu_switch</span>
<span class="c1"># […]</span>
<span class="nt">ips</span><span class="p">:</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">device</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">ad2-p6.example.com</span>
<span class="w">    </span><span class="nt">ip</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">172.31.115.18/21</span>
<span class="w">    </span><span class="nt">interface</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">oob</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">device</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">to1-p6.example.com</span>
<span class="w">    </span><span class="nt">ip</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">172.31.115.33/21</span>
<span class="w">    </span><span class="nt">interface</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">oob</span>
<span class="w">  </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">device</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">to1-p6.example.com</span>
<span class="w">    </span><span class="nt">ip</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">172.31.254.33/32</span>
<span class="w">    </span><span class="nt">interface</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">lo0.0</span>
<span class="c1"># […]</span>
</code></pre></div>

<p>L’équipe réseau n’est pas le seul utilisateur de <em>NetBox</em>. Si l’ajout de
nouveaux objets ou la modification d’objets existants est peu risqué,
la suppression d’objets indésirables peut être hasardeuse. Le module
ne supprime que les objets qu’il a créés ou modifiés. Pour les
identifier, il les marque avec une étiquette spécifique, <code>cmdb</code>. La
plupart des objets de <em>NetBox</em> acceptent les étiquettes.</p>
<h2 id="definition-du-module">Définition du module</h2>
<p>En se basant sur le squelette présenté dans l’<a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">article
précédent</a>, nous définissons le
module :</p>
<div class="language-python codehilite"><pre><span/><code><span class="n">module_args</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="n">source</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'path'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
    <span class="n">api</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'str'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
    <span class="n">token</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'str'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">no_log</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
    <span class="n">max_workers</span><span class="o">=</span><span class="nb">dict</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s1">'int'</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
<span class="p">)</span>

<span class="n">result</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="n">changed</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>

<span class="n">module</span> <span class="o">=</span> <span class="n">AnsibleModule</span><span class="p">(</span>
    <span class="n">argument_spec</span><span class="o">=</span><span class="n">module_args</span><span class="p">,</span>
    <span class="n">supports_check_mode</span><span class="o">=</span><span class="kc">True</span>
<span class="p">)</span>
</code></pre></div>

<p>Il contient un argument supplémentaire optionnel définissant le nombre
de requêtes à envoyer en parallèle à <em>NetBox</em> pour la récupération des
objets existants afin d’accélérer l’exécution.</p>
<h2 id="abstraction-du-processus-de-synchronisation">Abstraction du processus de synchronisation</h2>
<p>Nous devons synchroniser différents types d’objets, mais une fois que
nous avons une liste des objets que nous voulons dans <em>NetBox</em>, le
travail est toujours le même :</p>
<ul>
<li>vérifier si les objets existent déjà ;</li>
<li>les récupérer et les mettre sous une forme permettant de les comparer ;</li>
<li>récupérer les objets dont nous ne voulons plus ;</li>
<li>comparer les deux ensembles ;</li>
<li>ajouter les objets manquants, mettre à jour les objets existants,
  supprimer les objets en trop.</li>
</ul>
<p>Ces comportements sont codés dans une classe abstraite <code>Synchronizer</code>.
Pour chaque type d’objet, une classe concrète est construite avec les
<a href="https://www.attrs.org/en/stable/index.html" title="attrs: Classes Without Boilerplate">attributs</a> appropriés pour adapter son comportement
et une méthode <code>wanted()</code> pour fournir les objets désirés.</p>
<p>Je ne détaille pas ici le code de la classe abstraite. Jetez un coup
d’œil aux <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">sources</a> si nécessaire.</p>
<h3 id="synchroniser-les-etiquettes-et-les-tenants">Synchroniser les étiquettes et les tenants</h3>
<p>Pour commencer, voici comment nous définissons la classe synchronisant
les étiquettes :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncTags</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>
    <span class="n">app</span> <span class="o">=</span> <span class="s2">"extras"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"tags"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">{</span><span class="s2">"cmdb"</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span>
            <span class="n">slug</span><span class="o">=</span><span class="s2">"cmdb"</span><span class="p">,</span>
            <span class="n">color</span><span class="o">=</span><span class="s2">"8bc34a"</span><span class="p">,</span>
            <span class="n">description</span><span class="o">=</span><span class="s2">"synced by network CMDB"</span><span class="p">)}</span>
</code></pre></div>

<p>Les attributs <code>app</code> et <code>table</code> définissent les objets <em>NetBox</em> que
nous voulons manipuler. L’attribut <code>key</code> est utilisé pour déterminer
comment rechercher des objets existants. Dans cet exemple, nous
voulons rechercher des étiquettes en utilisant leurs noms.</p>
<p>La méthode <code>wanted()</code> doit renvoyer un dictionnaire qui associe les
clés des objets à la liste des attributs désirés. Ici, les clés sont
le nom des étiquettes et nous ne créons qu’une seule étiquette,
<code>cmdb</code>, avec la couleur et la description fournies. C’est cette
étiquette que nous utiliserons pour marquer les objets que nous
créerons ou modifierons.</p>
<p>Si l’étiquette n’existe pas, elle est créée. Si elle existe, les
attributs fournis sont mis à jour. Les autres attributs ne sont pas
modifiés.</p>
<p>Nous voulons également créer un tenant spécifique pour les objets
acceptant un tel attribut (équipements et adresses IP) :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncTenants</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>
    <span class="n">app</span> <span class="o">=</span> <span class="s2">"tenancy"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"tenants"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">{</span><span class="s2">"Network"</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">slug</span><span class="o">=</span><span class="s2">"network"</span><span class="p">,</span>
                                <span class="n">description</span><span class="o">=</span><span class="s2">"Network team"</span><span class="p">)}</span>
</code></pre></div>

<h3 id="synchroniser-les-sites">Synchroniser les sites</h3>
<p>Nous devons également synchroniser la liste des sites. Cette fois, la
méthode <code>wanted()</code> utilise les informations fournies dans le fichier
YAML : elle parcourt les équipements et construit la liste des centres
de données.</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncSites</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>

    <span class="n">app</span> <span class="o">=</span> <span class="s2">"dcim"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"sites"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>
    <span class="n">only_on_create</span> <span class="o">=</span> <span class="p">(</span><span class="s2">"status"</span><span class="p">,</span> <span class="s2">"slug"</span><span class="p">)</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">result</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">details</span><span class="p">[</span><span class="s2">"datacenter"</span><span class="p">]</span>
                     <span class="k">for</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'devices'</span><span class="p">]</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
                     <span class="k">if</span> <span class="s2">"datacenter"</span> <span class="ow">in</span> <span class="n">details</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">slug</span><span class="o">=</span><span class="n">k</span><span class="p">,</span>
                        <span class="n">status</span><span class="o">=</span><span class="s2">"planned"</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">result</span><span class="p">}</span>
</code></pre></div>

<p>Grâce à l’utilisation de l’attribut <code>only_on_create</code>, les attributs
spécifiés ne sont pas mis à jour s’ils sont différents. Le but de ce
synchroniseur est surtout de collecter les références aux différents
sites pour les utiliser dans d’autres objets.</p>
<div class="language-python-console-session codehilite"><pre><span/><code><span class="gp">&gt;&gt;&gt; </span><span class="n">pprint</span><span class="p">(</span><span class="n">SyncSites</span><span class="p">(</span><span class="o">**</span><span class="n">sync_args</span><span class="p">)</span><span class="o">.</span><span class="n">wanted</span><span class="p">())</span>
<span class="go">{'sfo1': {'slug': 'sfo1', 'status': 'planned'},</span>
<span class="go"> 'chi1': {'slug': 'chi1', 'status': 'planned'},</span>
<span class="go"> 'nyc1': {'slug': 'nyc1', 'status': 'planned'}}</span>
</code></pre></div>

<h3 id="synchroniser-les-constructeurs-les-types-dequipements-et-les-roles-des-equipements">Synchroniser les constructeurs, les types d’équipements et les rôles des équipements</h3>
<p>La synchronisation des constructeurs est très similaire, sauf que nous
n’avons pas besoin de l’attribut <code>only_on_create</code> :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncManufacturers</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>

    <span class="n">app</span> <span class="o">=</span> <span class="s2">"dcim"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"manufacturers"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">result</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">details</span><span class="p">[</span><span class="s2">"manufacturer"</span><span class="p">]</span>
                     <span class="k">for</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'devices'</span><span class="p">]</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
                     <span class="k">if</span> <span class="s2">"manufacturer"</span> <span class="ow">in</span> <span class="n">details</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="p">{</span><span class="s2">"slug"</span><span class="p">:</span> <span class="n">slugify</span><span class="p">(</span><span class="n">k</span><span class="p">)}</span>
                <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">result</span><span class="p">}</span>
</code></pre></div>

<p>Concernant les types d’équipements, nous utilisons l’attribut
<code>foreign</code> indiquant le lien entre un attribut <em>NetBox</em> et le
synchroniseur qui le gère.</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncDeviceTypes</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>

    <span class="n">app</span> <span class="o">=</span> <span class="s2">"dcim"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"device_types"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"model"</span>
    <span class="n">foreign</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"manufacturer"</span><span class="p">:</span> <span class="n">SyncManufacturers</span><span class="p">}</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">result</span> <span class="o">=</span> <span class="nb">set</span><span class="p">((</span><span class="n">details</span><span class="p">[</span><span class="s2">"manufacturer"</span><span class="p">],</span> <span class="n">details</span><span class="p">[</span><span class="s2">"model"</span><span class="p">])</span>
                     <span class="k">for</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'devices'</span><span class="p">]</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
                     <span class="k">if</span> <span class="s2">"model"</span> <span class="ow">in</span> <span class="n">details</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span><span class="n">k</span><span class="p">[</span><span class="mi">1</span><span class="p">]:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">manufacturer</span><span class="o">=</span><span class="n">k</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
                           <span class="n">slug</span><span class="o">=</span><span class="n">slugify</span><span class="p">(</span><span class="n">k</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span>
                <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">result</span><span class="p">}</span>
</code></pre></div>

<p>La méthode <code>wanted()</code> renvoie des références aux constructeurs en
utilisant les valeurs contenues dans l’attribut <code>key</code>. Dans ce cas, il
s’agit du nom du constructeur.</p>
<div class="language-python-console-session codehilite"><pre><span/><code><span class="gp">&gt;&gt;&gt; </span><span class="n">pprint</span><span class="p">(</span><span class="n">SyncManufacturers</span><span class="p">(</span><span class="o">**</span><span class="n">sync_args</span><span class="p">)</span><span class="o">.</span><span class="n">wanted</span><span class="p">())</span>
<span class="go">{'Cisco': {'slug': 'cisco'},</span>
<span class="go"> 'Dell': {'slug': 'dell'},</span>
<span class="go"> 'Juniper': {'slug': 'juniper'}}</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">pprint</span><span class="p">(</span><span class="n">SyncDeviceTypes</span><span class="p">(</span><span class="o">**</span><span class="n">sync_args</span><span class="p">)</span><span class="o">.</span><span class="n">wanted</span><span class="p">())</span>
<span class="go">{'ASR 9001': {'manufacturer': 'Cisco', 'slug': 'asr-9001'},</span>
<span class="go"> 'Catalyst 2960G-48TC-L': {'manufacturer': 'Cisco',</span>
<span class="go">                           'slug': 'catalyst-2960g-48tc-l'},</span>
<span class="go"> 'MX10003': {'manufacturer': 'Juniper', 'slug': 'mx10003'},</span>
<span class="go"> 'QFX10002-36Q': {'manufacturer': 'Juniper', 'slug': 'qfx10002-36q'},</span>
<span class="go"> 'QFX10002-72Q': {'manufacturer': 'Juniper', 'slug': 'qfx10002-72q'},</span>
<span class="go"> 'QFX5110-32Q': {'manufacturer': 'Juniper', 'slug': 'qfx5110-32q'},</span>
<span class="go"> 'QFX5110-48S': {'manufacturer': 'Juniper', 'slug': 'qfx5110-48s'},</span>
<span class="go"> 'QFX5200-32C': {'manufacturer': 'Juniper', 'slug': 'qfx5200-32c'},</span>
<span class="go"> 'S4048-ON': {'manufacturer': 'Dell', 'slug': 's4048-on'},</span>
<span class="go"> 'S6010-ON': {'manufacturer': 'Dell', 'slug': 's6010-on'}}</span>
</code></pre></div>

<p>Les rôles des équipements sont, quant à eux, définis ainsi :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncDeviceRoles</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>

    <span class="n">app</span> <span class="o">=</span> <span class="s2">"dcim"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"device_roles"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">result</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">details</span><span class="p">[</span><span class="s2">"role"</span><span class="p">]</span>
                     <span class="k">for</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'devices'</span><span class="p">]</span><span class="o">.</span><span class="n">values</span><span class="p">()</span>
                     <span class="k">if</span> <span class="s2">"role"</span> <span class="ow">in</span> <span class="n">details</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">slug</span><span class="o">=</span><span class="n">slugify</span><span class="p">(</span><span class="n">k</span><span class="p">),</span>
                        <span class="n">color</span><span class="o">=</span><span class="s2">"8bc34a"</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">result</span><span class="p">}</span>
</code></pre></div>

<h3 id="synchroniser-les-equipements">Synchroniser les équipements</h3>
<p>Un équipement est essentiellement un nom avec des références à un
rôle, un modèle, un site et un tenant. Ces références sont déclarées
comme des clés étrangères en référence aux synchroniseurs définis
précédemment.</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncDevices</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>
    <span class="n">app</span> <span class="o">=</span> <span class="s2">"dcim"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"devices"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"name"</span>
    <span class="n">foreign</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"device_role"</span><span class="p">:</span> <span class="n">SyncDeviceRoles</span><span class="p">,</span>
               <span class="s2">"device_type"</span><span class="p">:</span> <span class="n">SyncDeviceTypes</span><span class="p">,</span>
               <span class="s2">"site"</span><span class="p">:</span> <span class="n">SyncSites</span><span class="p">,</span>
               <span class="s2">"tenant"</span><span class="p">:</span> <span class="n">SyncTenants</span><span class="p">}</span>
    <span class="n">remove_unused</span> <span class="o">=</span> <span class="mi">10</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="nb">dict</span><span class="p">(</span><span class="n">device_role</span><span class="o">=</span><span class="n">details</span><span class="p">[</span><span class="s2">"role"</span><span class="p">],</span>
                           <span class="n">device_type</span><span class="o">=</span><span class="n">details</span><span class="p">[</span><span class="s2">"model"</span><span class="p">],</span>
                           <span class="n">site</span><span class="o">=</span><span class="n">details</span><span class="p">[</span><span class="s2">"datacenter"</span><span class="p">],</span>
                           <span class="n">tenant</span><span class="o">=</span><span class="s2">"Network"</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'devices'</span><span class="p">]</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
                <span class="k">if</span> <span class="p">{</span><span class="s2">"datacenter"</span><span class="p">,</span> <span class="s2">"model"</span><span class="p">,</span> <span class="s2">"role"</span><span class="p">}</span> <span class="o">&lt;=</span> <span class="nb">set</span><span class="p">(</span><span class="n">details</span><span class="o">.</span><span class="n">keys</span><span class="p">())}</span>
</code></pre></div>

<p>L’attribut <code>remove_unused</code> est un garde-fou provoquant un échec si
nous devons supprimer plus de 10 équipements : cela peut être
l’indication qu’il y a un problème quelque part ou un important départ
de feu dans l’un des sites.</p>
<div class="language-python-console-session codehilite"><pre><span/><code><span class="gp">&gt;&gt;&gt; </span><span class="n">pprint</span><span class="p">(</span><span class="n">SyncDevices</span><span class="p">(</span><span class="o">**</span><span class="n">sync_args</span><span class="p">)</span><span class="o">.</span><span class="n">wanted</span><span class="p">())</span>
<span class="go">{'ad2-p6.sfo1.example.com': {'device_role': 'net_tor_oob_switch',</span>
<span class="go">                             'device_type': 'Catalyst 2960G-48TC-L',</span>
<span class="go">                             'site': 'sfo1',</span>
<span class="go">                             'tenant': 'Network'},</span>
<span class="go"> 'to1-p6.sfo1.example.com': {'device_role': 'net_tor_gpu_switch',</span>
<span class="go">                             'device_type': 'QFX5110-48S',</span>
<span class="go">                             'site': 'sfo1',</span>
<span class="go">                             'tenant': 'Network'},</span>
<span class="go">[…]</span>
</code></pre></div>

<h3 id="synchroniser-les-adresses-ip">Synchroniser les adresses IP</h3>
<p>La dernière étape consiste à synchroniser les adresses IP. Nous ne les
attachons pas à un équipement<sup id="fnref:devices"><a class="footnote-ref" href="#fn:devices">2</a></sup>. Nous spécifions plutôt leurs
noms dans la description :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">class</span><span class="w"> </span><span class="nc">SyncIPs</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">):</span>
    <span class="n">app</span> <span class="o">=</span> <span class="s2">"ipam"</span>
    <span class="n">table</span> <span class="o">=</span> <span class="s2">"ip-addresses"</span>
    <span class="n">key</span> <span class="o">=</span> <span class="s2">"address"</span>
    <span class="n">foreign</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"tenant"</span><span class="p">:</span> <span class="n">SyncTenants</span><span class="p">}</span>
    <span class="n">remove_unused</span> <span class="o">=</span> <span class="mi">1000</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">wanted</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">wanted</span> <span class="o">=</span> <span class="p">{}</span>
        <span class="k">for</span> <span class="n">details</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">[</span><span class="s1">'ips'</span><span class="p">]:</span>
            <span class="k">if</span> <span class="n">details</span><span class="p">[</span><span class="s1">'ip'</span><span class="p">]</span> <span class="ow">in</span> <span class="n">wanted</span><span class="p">:</span>
                <span class="n">wanted</span><span class="p">[</span><span class="n">details</span><span class="p">[</span><span class="s1">'ip'</span><span class="p">]][</span><span class="s1">'description'</span><span class="p">]</span> <span class="o">=</span> \
                    <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">details</span><span class="p">[</span><span class="s1">'device'</span><span class="p">]</span><span class="si">}</span><span class="s2"> (and others)"</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">wanted</span><span class="p">[</span><span class="n">details</span><span class="p">[</span><span class="s1">'ip'</span><span class="p">]]</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
                    <span class="n">tenant</span><span class="o">=</span><span class="s2">"Network"</span><span class="p">,</span>
                    <span class="n">status</span><span class="o">=</span><span class="s2">"active"</span><span class="p">,</span>
                    <span class="n">dns_name</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span>        <span class="c1"># information is present in DNS</span>
                    <span class="n">description</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">details</span><span class="p">[</span><span class="s1">'device'</span><span class="p">]</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">details</span><span class="p">[</span><span class="s1">'interface'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
                    <span class="n">role</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
                    <span class="n">vrf</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">wanted</span>
</code></pre></div>

<p>Il y a une petite difficulté : <em>NetBox</em> autorise les adresses IP en
double. Une simple recherche ne suffit donc pas. En cas de multiples
résultats, nous choisissons le meilleur en préférant ceux marqués par
l’étiquette <code>cmdb</code>, puis ceux déjà attachés à une interface :</p>
<div class="language-python codehilite"><pre><span/><code><span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
<span class="w">    </span><span class="sd">"""Grab IP address from NetBox."""</span>
    <span class="c1"># There may be duplicate. We need to grab the "best."</span>
    <span class="n">results</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">Synchronizer</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="kc">None</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="n">scores</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">*</span><span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">result</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">results</span><span class="p">):</span>
        <span class="k">if</span> <span class="s2">"cmdb"</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">tags</span><span class="p">:</span>
            <span class="n">scores</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">10</span>
        <span class="k">if</span> <span class="n">result</span><span class="o">.</span><span class="n">interface</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
            <span class="n">scores</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">5</span>
    <span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">results</span><span class="p">),</span>
                  <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">k</span><span class="p">:</span> <span class="n">k</span><span class="p">[</span><span class="mi">0</span><span class="p">])[</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span>
</code></pre></div>

<h2 id="calculer-les-etats-actuels-et-attendus">Calculer les états actuels et attendus</h2>
<p>Chaque synchroniseur est initialisé avec une référence au module
Ansible, une référence à <a href="https://pynetbox.readthedocs.io/en/latest/">pynetbox</a>, les données contenues dans le
fichier YAML fourni et deux dictionnaires vides pour les états actuels
et attendus :</p>
<div class="language-python codehilite"><pre><span/><code><span class="n">source</span> <span class="o">=</span> <span class="n">yaml</span><span class="o">.</span><span class="n">safe_load</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">params</span><span class="p">[</span><span class="s1">'source'</span><span class="p">]))</span>
<span class="n">netbox</span> <span class="o">=</span> <span class="n">pynetbox</span><span class="o">.</span><span class="n">api</span><span class="p">(</span><span class="n">module</span><span class="o">.</span><span class="n">params</span><span class="p">[</span><span class="s1">'api'</span><span class="p">],</span>
                      <span class="n">token</span><span class="o">=</span><span class="n">module</span><span class="o">.</span><span class="n">params</span><span class="p">[</span><span class="s1">'token'</span><span class="p">])</span>

<span class="n">sync_args</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
    <span class="n">module</span><span class="o">=</span><span class="n">module</span><span class="p">,</span>
    <span class="n">netbox</span><span class="o">=</span><span class="n">netbox</span><span class="p">,</span>
    <span class="n">source</span><span class="o">=</span><span class="n">source</span><span class="p">,</span>
    <span class="n">before</span><span class="o">=</span><span class="p">{},</span>
    <span class="n">after</span><span class="o">=</span><span class="p">{}</span>
<span class="p">)</span>
<span class="n">synchronizers</span> <span class="o">=</span> <span class="p">[</span><span class="n">synchronizer</span><span class="p">(</span><span class="o">**</span><span class="n">sync_args</span><span class="p">)</span> <span class="k">for</span> <span class="n">synchronizer</span> <span class="ow">in</span> <span class="p">[</span>
    <span class="n">SyncTags</span><span class="p">,</span>
    <span class="n">SyncTenants</span><span class="p">,</span>
    <span class="n">SyncSites</span><span class="p">,</span>
    <span class="n">SyncManufacturers</span><span class="p">,</span>
    <span class="n">SyncDeviceTypes</span><span class="p">,</span>
    <span class="n">SyncDeviceRoles</span><span class="p">,</span>
    <span class="n">SyncDevices</span><span class="p">,</span>
    <span class="n">SyncIPs</span>
<span class="p">]]</span>
</code></pre></div>

<p>Chaque synchroniseur a une méthode <code>prepare()</code> dont le but est de
calculer l’état source et l’état cible. Elle renvoie <code>True</code> en cas de
différence.</p>
<div class="language-python codehilite"><pre><span/><code><span class="c1"># Check what needs to be synchronized</span>
<span class="k">try</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">synchronizer</span> <span class="ow">in</span> <span class="n">synchronizers</span><span class="p">:</span>
        <span class="n">result</span><span class="p">[</span><span class="s1">'changed'</span><span class="p">]</span> <span class="o">|=</span> <span class="n">synchronizer</span><span class="o">.</span><span class="n">prepare</span><span class="p">()</span>
<span class="k">except</span> <span class="n">AnsibleError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
    <span class="n">result</span><span class="p">[</span><span class="s1">'msg'</span><span class="p">]</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">message</span>
    <span class="n">module</span><span class="o">.</span><span class="n">fail_json</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>
</code></pre></div>

<h2 id="appliquer-les-changements">Appliquer les changements</h2>
<p>Pour revenir au squelette décrit dans l’<a href="/fr/blog/2020-ecrire-module-ansible" title="Écrire un module Ansible sur mesure">article précédent</a>, la dernière étape est d’appliquer les
changements quand une différence est détectée. Chaque synchroniseur
consigne les états actuels et attendus dans, respectivement,
<code>sync_args["before"][table]</code> et <code>sync_args["after"][table]</code> où <code>table</code>
est le nom de la table correspondant au type d’objet <em>NetBox</em>. L’objet
<code>diff</code> est un peu plus élaboré car il est construit table par table.
Ansible affiche alors le nom de chaque table avant la représentation
de la différence :</p>
<div class="language-python codehilite"><pre><span/><code><span class="c1"># Compute the diff</span>
<span class="k">if</span> <span class="n">module</span><span class="o">.</span><span class="n">_diff</span> <span class="ow">and</span> <span class="n">result</span><span class="p">[</span><span class="s1">'changed'</span><span class="p">]:</span>
    <span class="n">result</span><span class="p">[</span><span class="s1">'diff'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
        <span class="nb">dict</span><span class="p">(</span>
            <span class="n">before_header</span><span class="o">=</span><span class="n">table</span><span class="p">,</span>
            <span class="n">after_header</span><span class="o">=</span><span class="n">table</span><span class="p">,</span>
            <span class="n">before</span><span class="o">=</span><span class="n">yaml</span><span class="o">.</span><span class="n">safe_dump</span><span class="p">(</span><span class="n">sync_args</span><span class="p">[</span><span class="s2">"before"</span><span class="p">][</span><span class="n">table</span><span class="p">]),</span>
            <span class="n">after</span><span class="o">=</span><span class="n">yaml</span><span class="o">.</span><span class="n">safe_dump</span><span class="p">(</span><span class="n">sync_args</span><span class="p">[</span><span class="s2">"after"</span><span class="p">][</span><span class="n">table</span><span class="p">]))</span>
        <span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="n">sync_args</span><span class="p">[</span><span class="s2">"after"</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">sync_args</span><span class="p">[</span><span class="s2">"before"</span><span class="p">][</span><span class="n">table</span><span class="p">]</span> <span class="o">!=</span> <span class="n">sync_args</span><span class="p">[</span><span class="s2">"after"</span><span class="p">][</span><span class="n">table</span><span class="p">]</span>
    <span class="p">]</span>

<span class="c1"># Stop here if check mode is enabled or if no change</span>
<span class="k">if</span> <span class="n">module</span><span class="o">.</span><span class="n">check_mode</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">result</span><span class="p">[</span><span class="s1">'changed'</span><span class="p">]:</span>
    <span class="n">module</span><span class="o">.</span><span class="n">exit_json</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>
</code></pre></div>

<p>Chaque synchroniseur expose également une méthode <code>synchronize()</code> pour
appliquer les changements et une méthode <code>cleanup()</code> pour supprimer
les objets indésirables. L’ordre est important en raison de la
relation entre les objets.</p>
<div class="language-python codehilite"><pre><span/><code><span class="c1"># Synchronize</span>
<span class="k">for</span> <span class="n">synchronizer</span> <span class="ow">in</span> <span class="n">synchronizers</span><span class="p">:</span>
    <span class="n">synchronizer</span><span class="o">.</span><span class="n">synchronize</span><span class="p">()</span>
<span class="k">for</span> <span class="n">synchronizer</span> <span class="ow">in</span> <span class="n">synchronizers</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]:</span>
    <span class="n">synchronizer</span><span class="o">.</span><span class="n">cleanup</span><span class="p">()</span>
<span class="n">module</span><span class="o">.</span><span class="n">exit_json</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>
</code></pre></div>

<hr/>
<p>Le code complet est disponible sur <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">GitHub</a>. Par rapport à
l’utilisation de la <a href="https://netbox-ansible-collection.readthedocs.io/en/latest/" title="netbox.netbox Ansible Collection's documentation">collection <code>netbox.netbox</code></a>, la
logique est écrite en Python au lieu d’essayer d’assembler des tâches
Ansible. Je pense que cela est à la fois plus flexible et plus facile
à lire, notamment lorsque l’on essaie de supprimer des objets
obsolètes. Bien que je n’aie pas testé, cela devrait également être
plus rapide.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (12.2020)</p>
<p>« <a href="https://www.redhat.com/en/blog/using-netbox-for-ansible-source-of-truth">Using NetBox for Ansible Source of Truth</a> » présente comment mettre à
jour NetBox en utilisant la collection <code>netbox.netbox</code>. Vous pouvez comparer
le code avec la solution que je propose ici. La principale différence entre
les deux est le nettoyage des objets obsolètes qui est absent de la première
solution.</p>
</div>
<p>Une alternative aurait été de réutiliser le code de la collection
<code>netbox.netbox</code>, car il contient des primitives similaires. Malheureusement, je
n’y ai pas pensé jusqu’à présent. 😶</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:cmdb">
<p>À mon avis, une bonne option pour une <em>source de vérité</em> est
d’utiliser des fichiers YAML dans un dépôt <em>Git</em>. La gestion des
versions est offerte et l’utilisation d’un éditeur de texte est le
seul savoir-faire nécessaire. <a class="footnote-backref" href="#fnref:cmdb" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:devices">
<p>Cette lacune est principalement de la feignantise : nous
ne nous soucions pas vraiment de cette information. Notre
principale motivation pour mettre des adresses IP dans <em>NetBox</em>
est de garder une trace des adresses IP utilisées. Cependant, si
une adresse IP est déjà attachée à une interface, nous ne touchons
pas à cette association. <a class="footnote-backref" href="#fnref:devices" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>