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

  <entry>
    <title type="html">Offline PKI using 3 YubiKeys and an ARM single board computer</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2025-offline-pki-yubikeys" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2025-offline-pki-yubikeys#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-03-17T07:12:54Z</updated>
    <id>http://www.luffy.cx/en/blog/2025-offline-pki-yubikeys.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>An offline <abbr title="Public Key Infrastructure">PKI</abbr> enhances security by physically isolating the certificate
authority from network threats. A <a href="https://www.yubico.com/products/yubikey-5-overview/" title="Discover YubiKey 5">YubiKey</a> is a low-cost solution to store a
root certificate. You also need an air-gapped environment to operate the root
<abbr title="Certificate Authority">CA</abbr>.</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 relying on a set of 3 YubiKeys: 2 for the root CA and 1 for the&#10;intermediate CA." 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>Offline PKI backed up by 3 YubiKeys</figcaption></figure>
<p>This post describes an offline <abbr title="Public Key Infrastructure">PKI</abbr> system using the following components:</p>
<ul>
<li>2 YubiKeys for the <strong>root <abbr title="Certificate Authority">CA</abbr></strong> (with a 20-year validity),</li>
<li>1 YubiKey for the <strong>intermediate <abbr title="Certificate Authority">CA</abbr></strong> (with a 5-year validity), and</li>
<li>1 <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre Computer Sweet Potato</a> as an <strong>air-gapped <abbr title="Single Board Computer">SBC</abbr></strong>.</li>
</ul>
<p>It is possible to add more YubiKeys as a backup of the root <abbr title="Certificate Authority">CA</abbr> if needed. This
is not needed for the intermediate <abbr title="Certificate Authority">CA</abbr> as you can generate a new one if
the current one gets destroyed.</p>
<div class="toc">
<ul>
<li><a href="#the-software-part">The software part</a></li>
<li><a href="#the-hardware-part">The hardware part</a></li>
<li><a href="#the-nix-glue">The Nix glue</a></li>
</ul>
</div>
<h1 id="the-software-part">The software part</h1>
<p><a href="https://github.com/vincentbernat/offline-pki" title="Offline PKI system"><code>offline-pki</code></a> is a small Python application to manage an offline <abbr title="Public Key Infrastructure">PKI</abbr>.
It relies on <a href="https://github.com/Yubico/yubikey-manager" title="Python library for configuring any YubiKey">yubikey-manager</a> to manage YubiKeys and <a href="https://cryptography.io/en/latest/" title="cryptography's documentation">cryptography</a> for
cryptographic operations not executed on the YubiKeys. The application has some
opinionated design choices. Notably, the cryptography is hard-coded to use <a href="https://en.wikipedia.org/wiki/P-384" title="P-384 on Wikipedia">NIST
P-384 elliptic curve</a>.</p>
<p>The first step is to reset all your 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>Then, generate the root <abbr title="Certificate Authority">CA</abbr> and create as many copies as you want:</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>You can inspect the result:</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>Then, you can create an intermediate certificate with <code>offline-pki yubikey
intermediate</code> and use it to sign certificates by providing a <abbr title="Certificate Signing Request">CSR</abbr> to <code>offline-pki
certificate sign</code>. Be careful and inspect the <abbr title="Certificate Signing Request">CSR</abbr> before signing it, as only the
subject name can be overridden. Check the <a href="https://github.com/vincentbernat/offline-pki?tab=readme-ov-file#operations">documentation</a> for more details.
Get the available options using the <code>--help</code> flag.</p>
<h1 id="the-hardware-part">The hardware part</h1>
<p>To ensure the operations on the root and intermediate <abbr title="Certificate Authorities">CAs</abbr> are air-gapped,
a cost-efficient solution is to use an ARM64 single board computer. The <a href="https://libre.computer/products/aml-s905x-cc-v2/" title="Sweet Potato AML-S905X-CC-V2">Libre
Computer Sweet Potato</a> <abbr title="Single Board Computer">SBC</abbr> is a more open alternative to the well-known
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="Libre Computer Sweet Potato single board computer relying on the Amlogic S905X&#10;SOC" 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>Libre Computer Sweet Potato SBC, powered by the AML-S905X SOC</figcaption></figure>
<p>I interact with it through an <a href="https://www.amazon.com/dp/B07WX2DSVB" title="USB to TTL UART converter on Amazon">USB to TTL UART converter</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="the-nix-glue">The Nix glue</h1>
<p>To bring everything together, I am using <em>Nix</em> with a <a href="https://github.com/vincentbernat/offline-pki/blob/main/flake.nix">Flake</a> providing:</p>
<ul>
<li>a package for the <code>offline-pki</code> application, with shell completion,</li>
<li>a development shell, including an editable version of the <code>offline-pki</code> application,</li>
<li>a NixOS module to set up the offline <abbr title="Public Key Infrastructure">PKI</abbr>, resetting the system at each boot,</li>
<li>a QEMU image for testing, and</li>
<li>an SD card image to be used on the <em>Sweet Potato</em> or another ARM64 <abbr title="Single Board Computer">SBC</abbr>.</li>
</ul>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Execute the application locally</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"># Run the application inside a QEMU VM</span>
nix<span class="w"> </span>run<span class="w"> </span>github:vincentbernat/offline-pki<span class="se">\#</span>qemu
<span class="c1"># Build an SD card for the Sweet Potato or for the 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"># Get a development shell with the 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>The key for the root <abbr title="Certificate Authority">CA</abbr> is not generated by the YubiKey. Using an
air-gapped computer is all the more important. Put it in a safe with the
YubiKeys when done! <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">Short feedback on Cisco pyATS and Genie Parser</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2021-pyats-genie-parser" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2021-pyats-genie-parser#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-09-12T14:31:17Z</updated>
    <id>http://www.luffy.cx/en/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> is a framework for network automation and testing. It
includes, among other things, an open-source multi-vendor set of
parsers and models, <a href="https://github.com/CiscoTestAutomation/genieparser">Genie Parser</a>. It features 2700 parsers for
various commands over many network OS. On paper, this seems like a
great tool!</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 to parse outputs without connecting to a device</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>First disappointment: <em>pyATS</em> is closed-source with some exceptions. This
is quite annoying if you run into some issues outside <em>Genie Parser</em>.
For example, although <em>pyATS</em> uses the <code>ssh</code> command, it cannot
leverage my <code>ssh_config</code> file: <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">resolves hostnames before
providing them to <code>ssh</code></a>. There is <a href="https://github.com/CiscoTestAutomation/pyats#is-pyats-open-source">no plan to open source
<em>pyATS</em></a>. 🤨</p>
<p>Then, <em>Genie Parser</em> has two problems:</p>
<ol>
<li>The data models used are dependent on the vendor and OS,
   <del>despite the <a href="https://developer.cisco.com/docs/genie-docs/">documentation</a> saying otherwise</del>. For
   example, the data model used for IPv4 interfaces is different
   between <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/nxos/show_interface.py#L1144-L1226">NX-OS</a> and <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/iosxr/show_interface.py#L1184-L1219">IOS XR</a>.</li>
<li>The parsers rely on line-by-line regular expressions to extract
   data and some Python code as glue. This is fragile and may break
   silently.</li>
</ol>
<p>To illustrate the second point, let’s assume the output of <code>show ipv4
vrf all interface</code> is:</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>Because the <a href="https://github.com/CiscoTestAutomation/genieparser/blob/88cb6ee4ad540a45c9c0b03c8cd75271f39da424/src/genie/libs/parser/iosxr/show_interface.py#L1246-L1248">regular expression to parse an interface name</a> does not
expect the extra data after the interface state, <em>Genie Parser</em>
ignores the line starting the definition of <code>Loopback30</code> and parses
the output to this structure:<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>While this bug is simple to fix, this is an uphill battle. Any
existing or future slight variation in the output of a command could
trigger another similar undetected bug, despite the extended test
coverage. I have reported and fixed several other silent parsing
errors: <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>, and <a href="https://github.com/CiscoTestAutomation/genieparser/pull/530" title="CiscoTestAutomation/genieparser #530: IOSXR: fix AS parsing for dynamic neighbors">#530</a>. A more robust alternative
would have been to use <a href="https://github.com/google/textfsm" title="Python module for parsing semi-structured text into python tables">TextFSM</a> and to trigger a warning when some
output is not recognized, like <a href="https://batfish.org/" title="An open source network configuration analysis tool">Batfish</a>, a configuration analysis
tool, does.</p>
<p>In the future, we should rely on <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> for data extraction, but it
is currently not widely supported. <abbr title="Simple Network Management Protocol">SNMP</abbr> is still a valid possibility
but much information is not accessible through this protocol. In the
meantime, I would advise you to only use <em>Genie Parser</em> with caution.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:model">
<p>As an exercise, the astute reader is asked to write the code
to extract the IPv4 from this 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">Switching to the i3 window manager</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2021-i3-window-manager" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2021-i3-window-manager#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-09-06T05:45:47Z</updated>
    <id>http://www.luffy.cx/en/blog/2021-i3-window-manager.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>I have been using the <a href="https://awesomewm.org/">awesome window manager</a> for 10 years. It is a
<a href="https://en.wikipedia.org/wiki/Tiling_window_manager">tiling window manager</a>, configurable and extendable with the
<a href="https://www.lua.org/">Lua</a> language. Using a general-purpose programming language to
configure every aspect is a double-edged sword. Due to laziness and
the apparent difficulty of adapting <a href="https://github.com/vincentbernat/awesome-configuration/">my configuration</a>—about 3000 lines—to newer releases, I was stuck with the 3.4
version, whose last release is from 2013.</p>
<p>It was time for a rewrite. Instead, I have switched to the <a href="https://i3wm.org/">i3 window
manager</a>, lured by the possibility to migrate to <a href="https://wayland.freedesktop.org/">Wayland</a> and
<a href="https://swaywm.org/">Sway</a> later with minimal pain. Using an embedded interpreter for
configuration is not as important to me as it was in the past: it
brings both complexity and brittleness.</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 dual screen setup" 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>Dual screen desktop running i3, Emacs, some terminals, including a Quake console, Firefox, Polybar as the status bar, and Dunst as the notification daemon.</figcaption></figure>
<p>The window manager is only one part of a desktop environment. There
are several options for the other components. I am also introducing
them in this post.</p>
<div class="toc">
<ul>
<li><a href="#i3-the-window-manager">i3: the window manager</a></li>
<li><a href="#i3-companion-the-missing-bits">i3 companion: the missing bits</a></li>
<li><a href="#dunst-the-notification-daemon">dunst: the notification daemon</a></li>
<li><a href="#polybar-the-status-bar">polybar: the status bar</a></li>
<li><a href="#picom-the-compositor">picom: the compositor</a></li>
<li><a href="#systemd-the-service-manager">systemd: the service manager</a></li>
<li><a href="#rofi-the-application-launcher">rofi: the application launcher</a></li>
<li><a href="#xss-lock-and-i3lock-the-screen-locker">xss-lock and i3lock: the screen locker</a></li>
<li><a href="#the-remaining-components">The remaining components</a></li>
</ul>
</div>
<h1 id="i3-the-window-manager">i3: the window manager</h1>
<p><em>i3</em> aims to be a minimal tiling window manager. Its <a href="https://i3wm.org/docs/userguide.html" title="i3 User's Guide">documentation</a>
can be read from top to bottom in less than an hour. <em>i3</em> organizes
windows in a <a href="https://i3wm.org/docs/userguide.html#_tree" title="i3 User's Guide: Tree">tree</a>. Each non-leaf node contains one or several
windows and has an orientation and a layout. This information
arbitrates the window positions. <em>i3</em> features three layouts: split,
stacking, and tabbed. They are demonstrated in the below screenshot:</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="Example of layouts" 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>Demonstration of the layouts available in i3. The main container is split horizontally. The first child is split vertically. The second one is tabbed. The last one is stacking.</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="Tree representation of the previous&#10;screenshot" src="https://media-bernat-ch.b-cdn.net/images/i3/layouts.574313ba154777.svg" width="477" height="447" class="lf-media"/></span></div><figcaption>Tree representation of the previous screenshot.</figcaption></figure>
<p>Most of the other tiling window managers, including the <em>awesome
window manager</em>, use predefined layouts. They usually feature a large
area for the main window and another area divided among the remaining
windows. These layouts can be tuned a bit, but you mostly stick to a
couple of them. When a new window is added, the behavior is quite
predictable. Moreover, you can cycle through the various windows
without thinking too much as they are ordered.</p>
<p><em>i3</em> is more flexible with its ability to build any layout on the fly,
but it can feel quite overwhelming as you need to visualize the tree in
your head. At first, it is not unusual to find yourself with a complex
tree with many useless nested containers. Moreover, you have to
navigate windows using directions. It takes some time to get used to.</p>
<p>I set up a split layout for Emacs and a few terminals, but most of the
other workspaces are using a tabbed layout. I do not use the stacking
layout. You can find many scripts trying to emulate other tiling
window managers, but I did try to keep my setup free of these
attempts and get a chance to familiarize myself. <em>i3</em> can also <a href="https://i3wm.org/docs/layout-saving.html" title="Layout saving in i3">save
and restore layouts</a>, which is quite a powerful feature.</p>
<p><a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/config" title="i3 configuration file">My configuration</a> is quite similar to the default one and has
less than 200 lines.</p>
<h1 id="i3-companion-the-missing-bits">i3 companion: the missing bits</h1>
<p><em>i3</em> philosophy is to keep a minimal core and let the user implement
missing features using the <abbr title="Inter-process communication">IPC</abbr> protocol:</p>
<blockquote>
<p>Do not add further complexity when it can be avoided. We are
generally happy with the feature set of i3 and instead focus on
fixing bugs and maintaining it for stability. New features will
therefore only be considered if the benefit outweighs the additional
complexity, and we encourage users to implement features using the
<abbr title="Inter-process communication">IPC</abbr> whenever possible.</p>
<p>— <a href="https://i3wm.org/">Introduction to the i3 window manager</a></p>
</blockquote>
<p>While this is not as powerful as an embedded language, it is enough
for many cases. Moreover, as high-level features may be opinionated,
delegating them to small, loosely coupled pieces of code keeps them
more maintainable. Libraries exist for this purpose in <a href="https://i3wm.org/docs/ipc.html#_see_also_existing_libraries" title="IPC interface (interprocess communication): existing libraries">several
languages</a>. Users have published many scripts to extend <em>i3</em>:
<a href="https://aduros.com/blog/hacking-i3-automatic-layout/" title="Hacking i3: Automatic Layout">automatic layout</a> and <a href="https://aduros.com/blog/hacking-i3-window-promoting/" title="Hacking i3: Window Promoting">window promotion</a> to mimic the behavior
of other tiling window managers, <a href="https://aduros.com/blog/hacking-i3-window-swallowing/" title="Hacking i3: Window Swallowing">window swallowing</a> to put a new
app on top of the terminal launching it, and <a href="https://github.com/altdesktop/i3ipc-python/blob/master/examples/i3-cycle-focus.py">cycling between
windows</a> with <kbd>Alt</kbd>+<kbd>Tab</kbd>.</p>
<p>Instead of maintaining a script for each feature, I have centralized
everything into a single Python process,
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/i3-companion"><code>i3-companion</code></a> using <a href="https://docs.python.org/3/library/asyncio.html">asyncio</a> and the
<a href="https://i3ipc-python.readthedocs.io/en/latest/" title="i3ipc-python Documentation">i3ipc-python</a> library. Each feature is self-contained into a
function. It implements the following components:</p>
<dl>
<dt>Make a workspace exclusive to an application</dt>
<dd>When a workspace contains Emacs or Firefox, I would like other
  applications to move to another workspace, except for the terminal
  which is allowed to “intrude” into any workspace. The
  <code>workspace_exclusive()</code> function monitors new windows and moves them
  if needed to an empty workspace or to one with the same application
  already running.</dd>
<dt>Implement a Quake console</dt>
<dd>The <code>quake_console()</code> function implements a drop-down console
  available from any workspace. It can be toggled with
  <kbd>Mod</kbd>+<kbd>`</kbd>. This is implemented as a <a href="https://i3wm.org/docs/userguide.html#_scratchpad" title="i3's User Guide: Scratchpad">scratchpad
  window</a>.</dd>
<dt>Back and forth workspace switching on the same output</dt>
<dd>With the <code>workspace back_and_forth</code> command, we can ask <em>i3</em> to
  switch to the previous workspace. However, this feature is not
  restricted to the current output. I prefer to have one keybinding to
  switch to the workspace on the next output and one keybinding to
  switch to the previous workspace on the same output. This behavior
  is implemented in the <code>previous_workspace()</code> function by keeping a
  per-output history of the focused workspaces.</dd>
<dt>Create a new empty workspace or move a window to an empty workspace</dt>
<dd>To create a new empty workspace or move a window to an empty
  workspace, you have to locate a free slot and use <code>workspace number
  4</code> or <code>move container to workspace number 4</code>. The <code>new_workspace()</code>
  function finds a free number and uses it as the target workspace.</dd>
<dt>Restart some services on output change</dt>
<dd>When adding or removing an output, some actions need to be executed:
  refresh the wallpaper, restart some components unable to adapt their
  configuration on their own, etc. <em>i3</em> triggers an event for this
  purpose. The <code>output_update()</code> function also takes an extra step to
  coalesce multiple consecutive events and to check if there is a real
  change with the low-level library <a href="https://github.com/tych0/xcffib" title="drop-in replacement for xpyb based on cffi">xcffib</a>.</dd>
</dl>
<p>I will detail the other features as this post goes on. On the
technical side, each function is decorated with the events it should
react to:</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>The <code>CommandEvent()</code> event class is my way to send a command to the
companion, using either <code>i3-msg -t send_tick</code> or binding a key to a
<code>nop</code> command. The latter is used to avoid spawning a shell and a
<code>i3-msg</code> process just to send a message. The companion listens to
binding events and checks if this is a <code>nop</code> command.</p>
<div class="language-text-only codehilite"><pre><span/><code>bindsym $mod+Tab nop "previous-workspace"
</code></pre></div>

<p>There are other decorators to avoid code duplication: <code>@debounce()</code> to
coalesce multiple consecutive calls, <code>@static()</code> to define a static
variable, and <code>@retry()</code> to retry a function on failure. The <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/i3-companion">whole
script</a> is a bit more than 1000 lines. I think this is
worth a read as I am quite happy with the result. 🦚</p>
<div class="admonition">
<p class="admonition-title">Update (2022-07)</p>
<p><a href="https://github.com/kriansa">Daniel Pereira</a> wrote <a href="https://github.com/kriansa/wmcompanion">wmcompanion</a>, a
desktop event listener for minimal window manager users, inspired by
<em>i3-companion</em>. It is a generic engine to build your own companion. It
does not have built-in support for <em>i3</em> events, but its modular
design allows a motivated user to add it with a few lines of code.</p>
</div>
<h1 id="dunst-the-notification-daemon">dunst: the notification daemon</h1>
<p>Unlike the awesome window manager, <em>i3</em> does not come with a built-in
notification system. <a href="https://dunst-project.org/">Dunst</a> is a lightweight notification daemon. I
am running a modified version with <a href="https://github.com/dunst-project/dunst/pull/890" title="PR #890: X11: enable dpi scaling">HiDPI support for X11</a> and
<a href="https://github.com/dunst-project/dunst/pull/864" title="PR #864: Recursive icon lookup">recursive icon lookup</a>. The <em>i3 companion</em> has a helper function,
<code>notify()</code>, to send notifications using DBus. <code>container_info()</code> and
<code>workspace_info()</code> use it to display information about the container
or the tree for a workspace.</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 showing i3 tree for a&#10;workspace" 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 showing i3's tree for a workspace</figcaption></figure>
<h1 id="polybar-the-status-bar">polybar: the status bar</h1>
<p><em>i3</em> bundles <a href="https://i3wm.org/i3bar/">i3bar</a>, a versatile status bar, but I have opted for
<a href="https://github.com/polybar/polybar" title="Fast and easy-to-use bar">Polybar</a>. A <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/polybar">wrapper script</a> runs one instance for
each monitor.</p>
<p>The first module is the built-in support for <em>i3</em> workspaces. To not
have to remember which application is running in a workspace, the <em>i3
companion</em> renames workspaces to include an icon for each application.
This is done in the <code>workspace_rename()</code> function. The icons are from
the <a href="https://fontawesome.com/">Font Awesome</a> project. I maintain a mapping between applications
and icons. This is a bit cumbersome but it looks great.</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="i3 workspaces in 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>i3 workspaces in Polybar</figcaption></figure>
<p>For CPU, memory, brightness, battery, disk, and audio volume, I am
relying on the built-in modules. Polybar’s <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/polybar">wrapper script</a> generates the list of filesystems to monitor and they get only
displayed when available space is low. The battery widget turns red
and blinks slowly when running out of power. Check my <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/polybar.conf">Polybar
configuration</a> for more details.</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="Various modules for 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 displaying various information: CPU usage, memory usage, screen brightness, battery status, Bluetooth status (with a connected headset), network status (connected to a wireless network and to a VPN), notification status, and speaker volume.</figcaption></figure>
<p>For Bluetooth, network, and notification statuses, I am using Polybar’s
<a href="https://github.com/polybar/polybar/wiki/Module:-ipc"><code>ipc</code> module</a>: the next version of <em>Polybar</em> can <a href="https://github.com/polybar/polybar/pull/2463" title="PR #2463: feat(ipc): allow receiving arbitrary text on IPC socket">receive
an arbitrary text on an <abbr title="Inter-process communication">IPC</abbr> socket</a>. The module is defined with a
single hook to be executed at the start to restore the latest status.</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>It can be updated with <code>polybar-msg action "#network.send.XXXX"</code>. In
the <em>i3 companion</em>, the <code>@polybar()</code> decorator takes the string
returned by a function and pushes the update through the <abbr title="Inter-process communication">IPC</abbr> socket.</p>
<p>The <em>i3 companion</em> reacts to DBus signals to update the Bluetooth and
network icons. The <code>@on()</code> decorator accepts a <code>DBusSignal()</code> object:</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>The middle of the bar is occupied by the date and a weather forecast.
The latter also uses the <abbr title="Inter-process communication">IPC</abbr> mechanism, but the source is a <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/weather">Python
script</a> triggered by a timer.</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 and weather in 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>Current date and weather forecast for the day in Polybar. The data is retrieved with the OpenWeather API.</figcaption></figure>
<p>I do not use the <em>system tray</em> integrated with <em>Polybar</em>. The embedded
icons usually look horrible and they all behave differently. A few
years back, <a href="https://blogs.gnome.org/aday/2017/08/31/status-icons-and-gnome/" title="Status Icons and GNOME">GNOME has removed the system tray</a>. Most of the
problems are fixed by the DBus-based <a href="https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/">Status Notifier Item
protocol</a>—also known as <a href="https://web.archive.org/web/2021/https://wiki.ubuntu.com/DesktopExperienceTeam/ApplicationIndicators" title="Application Panel Indicators">Application Indicators</a> or <a href="https://ayatanaindicators.github.io/">Ayatana
Indicators</a> for GNOME. However, <a href="https://github.com/polybar/polybar/issues/2010" title="No support for SystemNotifier tray icons (electron apps)"><em>Polybar</em> does not support this
protocol</a>. In the <em>i3 companion</em>, the implementation of Bluetooth
and network icons, including the notifications on change, takes
about 200 lines. I got to learn a bit about how DBus works and I get exactly
the info I want.</p>
<h1 id="picom-the-compositor">picom: the compositor</h1>
<p>I like having slightly transparent backgrounds for terminals and to
reduce the opacity of unfocused windows. This requires a
compositor.<sup id="fnref:tearfree"><a class="footnote-ref" href="#fn:tearfree">1</a></sup> <a href="https://github.com/yshui/picom">picom</a> is a lightweight compositor. It works
well for me, but it may need some tweaking depending on your graphic
card.<sup id="fnref:intel"><a class="footnote-ref" href="#fn:intel">2</a></sup> Unlike the awesome window manager, <em>i3</em> does not handle
transparency, so the compositor needs to decide by itself the opacity
of each window. Check my <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/picom.conf">configuration</a> for details.</p>
<h1 id="systemd-the-service-manager">systemd: the service manager</h1>
<p>I use <a href="https://systemd.io">systemd</a> to start <em>i3</em> and the various services around it. My
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/xsession">xsession script</a> only sets some environment variables and lets
<em>systemd</em> handle everything else. Have a look at <a href="https://goral.net.pl/post/systemd-x-sessions/" title="Using systemd for X sessions">this article from
Michał Góral</a> for the rationale. Notably, each component can be
easily restarted and their logs are not mangled inside the
<code>~/.xsession-errors</code> file.<sup id="fnref:limitation"><a class="footnote-ref" href="#fn:limitation">3</a></sup></p>
<p>I am using a two-stage setup: <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/systemd/i3.service"><code>i3.service</code></a> depends on
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/systemd/xsession.target"><code>xsession.target</code></a> to start services before
<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>Then, <em>i3</em> executes the second stage by invoking the
<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>Have a look at my <a href="https://github.com/vincentbernat/i3wm-configuration/tree/master/dotfiles/systemd">configuration files</a> for more
details.</p>
<h1 id="rofi-the-application-launcher">rofi: the application launcher</h1>
<p><a href="https://github.com/davatorium/rofi/" title="Rofi: A window switcher, application launcher and dmenu replacement">Rofi</a> is an application launcher. Its appearance can be customized
through a <abbr title="Cascading Style Sheets">CSS</abbr>-like language and it comes with several themes. Have a
look at my <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/dotfiles/rofi.conf">configuration</a> for mine.</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 as an application launcher" 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 as an application launcher</figcaption></figure>
<p>It can also act as a generic menu application. I have a script
to <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/rofi-mediaplayer">control a media player</a> and another one to
<a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/rofi-wifi">select the wifi network</a>. It is quite a flexible
application.</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 as a wifi network selector" 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 to select a wireless network</figcaption></figure>
<h1 id="xss-lock-and-i3lock-the-screen-locker">xss-lock and i3lock: the screen locker</h1>
<p><a href="https://i3wm.org/i3lock/">i3lock</a> is a simple screen locker. <a href="https://bitbucket.org/raymonad/xss-lock/src/master/">xss-lock</a> invokes it reliably
on inactivity or before a system suspend. For inactivity, it uses the
<a href="https://manpages.debian.org/Xss.3.html" title="Manual page for Xss(3)">XScreenSaver events</a>. The delay is configured using the <code>xset s</code>
command. The locker can be invoked immediately with <code>xset s activate</code>.
X11 applications know how to prevent the screen saver from running. I
have also developed a small <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xss-dimmer">dimmer application</a> that is executed 20
seconds before the locker to give me a chance to move the mouse if I
am not away.<sup id="fnref:dimmer"><a class="footnote-ref" href="#fn:dimmer">4</a></sup> Have a look at my <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xss-lock">configuration
script</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>Demonstration of xss-lock, xss-dimmer and i3lock with a 4× speedup.</figcaption></figure>
<div class="admonition">
<p class="admonition-title">Update (2021-12)</p>
<p>I am now using <a href="https://github.com/google/xsecurelock" title="X11 screen lock utility with security in mind">XSecureLock</a> with a <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/xsecurelock-saver">Python
script</a> to customize the screen saver. See
“<a href="/en/blog/2021-xsecurelock">Custom screen saver with XSecureLock</a>” for more details.</p>
</div>
<h1 id="the-remaining-components">The remaining components</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> is a tool to detect the connected display, match them
  against a set of profiles, and configure them with <code>xrandr</code>.</p>
</li>
<li>
<p><a href="https://github.com/andrewshadura/inputplug" title="XInput event monitor daemon">inputplug</a> executes a script for each new mouse and keyboard
  plugged. This is quite useful to load the appropriate <a href="/en/extending-xkb" title="Extending the X keyboard map with xkb">keyboard
  map</a>. See my <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/input-event">configuration</a>.</p>
</li>
<li>
<p><a href="https://github.com/derat/xsettingsd" title="Provides settings to X11 applications via the XSETTINGS specification">xsettingsd</a> provides settings to X11 applications, not unlike
  <a href="https://manpages.debian.org/xrdb.1.html" title="Manual page for xrdb(1)">xrdb</a> but it notifies applications for changes. The main use is
  to configure the Gtk and DPI settings. See my article on <a href="/en/blog/2018-4k-hidpi-dual-screen-linux#hidpi-support-on-linux-with-x11">HiDPI
  support on Linux with X11</a>.</p>
</li>
<li>
<p><a href="https://github.com/sharpbracket/redshift">Redshift</a> adjusts the color temperature of the screen according
  to the time of day.</p>
</li>
<li>
<p><a href="https://github.com/naelstrof/maim">maim</a> is a utility to take screenshots. I use <kbd>Prt Scn</kbd>
  to trigger a screenshot of a window or a specific area and
  <kbd>Mod</kbd>+<kbd>Prt Scn</kbd> to capture the whole desktop to a
  file. Check the <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/screenshot">helper script</a> for details.</p>
</li>
<li>
<p>I have a collection of wallpapers I rotate every hour. A
  <a href="https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/wallpaper">script</a> selects them using advanced machine learning
  algorithms and stitches them together on multi-screen setups. The
  selected wallpaper is reused by <em>i3lock</em>.</p>
</li>
</ul>
<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>This post was heavily discussed on <a href="https://news.ycombinator.com/item?id=28430849">Hacker News</a> and
<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>Apart from the eye candy, a compositor also helps to get
tear-free video playbacks. <a class="footnote-backref" href="#fnref:tearfree" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:intel">
<p>My configuration works with both Haswell (2014) and Whiskey
Lake (2018) Intel GPUs. It also works with AMD GPU based on the
Polaris chipset (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>You cannot manage two different displays this way—e.g.
<code>:0</code> and <code>:1</code>. In the first implementation, I did try to
parametrize each service with the associated display, but this is
useless: there is only one DBus user session and many services
rely on it. For example, you cannot run two notification daemons. <a class="footnote-backref" href="#fnref:limitation" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:dimmer">
<p>I have only discovered later that <a href="https://github.com/google/xsecurelock" title="X11 screen lock utility with security in mind">XSecureLock</a> ships
such a dimmer with a similar implementation. But mine has a cool
countdown! Optionally, it can also fade to a background image. <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: a configuration management system for network</title>
    <author><name>ex-Blade Network Team</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2021-network-jerikan-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2021-network-jerikan-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2021-05-25T12:16:03Z</updated>
    <id>http://www.luffy.cx/en/blog/2021-network-jerikan-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>There are many resources for network automation with <a href="https://www.ansible.com" title="https://www.ansible.com">Ansible</a>. Most
of them only expose the <a href="https://docs.ansible.com/projects/ansible/latest/network/getting_started/index.html" title="Ansible for Network Automation">first steps</a> or limit themselves to a
<a href="https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules" title="NVIDIA Cumulus Linux Roles">narrow scope</a>. They give no clue on how to expand from that. Real
network environments may be large, versatile, heterogeneous, and filled
with exceptions. The lack of real-world examples for <em>Ansible</em>
deployments, unlike <a href="https://github.com/wikimedia/operations-puppet" title="Puppet repository for the Wikimedia Foundation">Puppet</a> and <a href="https://github.com/creativecommons/sre-salt-prime" title="Creative Commons SaltStack configuration files">SaltStack</a>, leads many teams to
build brittle and incomplete automation solutions.</p>
<p>We have <a href="https://github.com/jerikan-network/cmdb">released</a> under an open-source license our attempt
to tackle this problem:</p>
<ul>
<li><strong>Jerikan</strong>, a tool to build configuration files from a <em>single
   source of truth</em> and <em><a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a> templates</em>, along with its
   integration into the <strong>GitLab CI</strong> system;</li>
<li>an <strong>Ansible playbook</strong> to deploy these configuration files on
   network devices; and</li>
<li><strong>the configuration data and the templates</strong> for our, now defunct,
   datacenters in San Francisco and South Korea, covering many vendors
   (Facebook Wedge 100, Dell S4048 and S6010, Juniper QFX 5110,
   Juniper QFX 10002, Cisco ASR 9001, Cisco Catalyst 2960, Opengear
   console servers, and Linux), and many functionalities
   (provisioning, BGP-to-the-host routing, edge routing, out-of-band
   network, DNS configuration, integration with NetBox and <abbr title="Internet Routing Registries">IRRs</abbr>).</li>
</ul>
<p>Here is a quick demo to configure a new 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" default="" label="English"/><track src="https://media.bernat.ch/videos/2021-network-cmdb.fr.vtt" kind="subtitles" srclang="fr" label="français"/></video></span></div></figure>
<p>This work is the collective effort of <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>, and <a href="/en">Vincent Bernat</a>. We are the network team of
Blade, a French company operating <a href="https://shadow.tech">Shadow</a>, a cloud-computing
product. In May 2021, our company <a href="https://techcrunch.com/2021/04/30/cloud-gaming-service-shadow-taken-over-by-ovhcloud-founder/" title="Cloud gaming service Shadow taken over by OVHcloud founder">was bought by Octave Klaba</a> and
the infrastructure is being transferred to <a href="https://www.ovhcloud.com">OVHcloud</a>, saving
<em>Shadow</em> as a product, but making our team redundant.</p>
<p>Our network was around 800 devices, spanning over 10 datacenters with
more than 2.5 Tbps of available egress bandwidth. The released
material is therefore a substantial example of managing a medium-scale
network using <em>Ansible</em>. We have left out the handling of our legacy
datacenters to make the final result more readable while keeping
enough material to not turn it into a trivial example.</p>
<div class="toc">
<ul>
<li><a href="#jerikan">Jerikan</a><ul>
<li><a href="#source-of-truth">Source of truth</a></li>
<li><a href="#templates">Templates</a></li>
<li><a href="#checks">Checks</a></li>
<li><a href="#integration-into-gitlab-ci">Integration into GitLab CI</a></li>
</ul>
</li>
<li><a href="#ansible">Ansible</a><ul>
<li><a href="#inventory">Inventory</a></li>
<li><a href="#playbook">Playbook</a></li>
</ul>
</li>
<li><a href="#configuration-data-and-templates">Configuration data and templates</a></li>
</ul>
</div>
<h1 id="jerikan">Jerikan</h1>
<p>The first component is <em>Jerikan</em>. As input, it takes a list of
devices, configuration data, templates, and validation scripts. It
generates a set of configuration files for each device. <em>Ansible</em>
could cover this task, but it has the following limitations:</p>
<ul>
<li>it is slow;</li>
<li>errors are difficult to debug;<sup id="fnref:debug"><a class="footnote-ref" href="#fn:debug">1</a></sup> and</li>
<li>the hierarchy to look up a variable is <a href="https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence" title="Using Variables: Understanding variable precedence">rigid</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="Jerikan inputs and outputs" src="https://media-bernat-ch.b-cdn.net/images/jerikan/jerikan.41bd9d738ac96c.svg" width="830" height="596" class="lf-media"/></span></div><figcaption>Jerikan inputs and outputs</figcaption></figure>
<p>If you want to follow the examples, you only need to have <a href="https://docs.docker.com/" title="Docker documentation">Docker</a> and
<a href="https://docs.docker.com/compose/" title="Overview of Docker Compose">Docker Compose</a> installed. Clone the <a href="https://github.com/jerikan-network/cmdb">repository</a> and you
are ready!</p>
<h2 id="source-of-truth">Source of truth</h2>
<p>We use YAML files, versioned with <em>Git</em>, as the <strong>single source of
truth</strong> instead of using a database, like <em>NetBox</em>, or a mix of a
database and text files. This provides many advantages:</p>
<ul>
<li>anyone can use their preferred text editor;</li>
<li>the team prepares changes in branches;</li>
<li>the team reviews changes using merge requests;</li>
<li>the merge requests expose the changes to the generated configuration files;</li>
<li>rollback to a previous state is easy; and</li>
<li>it is fast.</li>
</ul>
<div class="admonition">
<p class="admonition-title">Update (2021-11)</p>
<p>See “<a href="/en/blog/2021-source-of-truth-network">Git as a source of truth for network
automation</a>” for more detailed arguments about choosing <em>Git</em>
instead of <em>NetBox</em>.</p>
</div>
<p>The first file is <a href="https://github.com/jerikan-network/cmdb/blob/main/devices.yaml"><code>devices.yaml</code></a>. It contains the
device list. The second file is <a href="https://github.com/jerikan-network/cmdb/blob/main/classifier.yaml"><code>classifier.yaml</code></a>.
It defines a scope for each device. A scope is a set of keys and
values. It is used in templates and to look up data associated with a
device.</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>The device name is matched against a list of regular expressions and
the scope is extended by the result of each match. For
<code>to1-p1.sk1.blade-group.net</code>, the following subset of
<code>classifier.yaml</code> defines its scope:</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>The third file is <a href="https://github.com/jerikan-network/cmdb/blob/main/searchpaths.py"><code>searchpaths.py</code></a>. It describes
which directories to search for a variable. A Python function provides
a list of paths to look up in <a href="https://github.com/jerikan-network/cmdb/tree/main/data"><code>data/</code></a> for a given scope. Here
is a simplified version:<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>With this definition, the data for <code>to1-p1.sk1.blade-group.net</code> is
looked up in the following paths:</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>Variables are scoped using a namespace that should be specified when
doing a lookup. We use the following ones:</p>
<ul>
<li><code>system</code> for accounts, DNS, syslog servers, …</li>
<li><code>topology</code> for ports, interfaces, IP addresses, subnets, …</li>
<li><code>bgp</code> for BGP configuration</li>
<li><code>build</code> for templates and validation scripts</li>
<li><code>apps</code> for application variables</li>
</ul>
<p>When looking up for a variable in a given namespace, <em>Jerikan</em> looks
for a YAML file named after the namespace in each directory in the
search paths. For example, if we look up a variable for
<code>to1-p1.sk1.blade-group.net</code> in the <code>bgp</code> namespace, the following
YAML files are processed: <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>, and <code>common/bgp.yaml</code>. The search stops at the
first match.</p>
<p>The <a href="https://github.com/jerikan-network/cmdb/blob/main/schema.yaml"><code>schema.yaml</code></a> file allows us to override this
behavior by asking to merge dictionaries and arrays across all
matching files.<sup id="fnref:schema"><a class="footnote-ref" href="#fn:schema">3</a></sup> Here is an excerpt of this file for the <code>topology</code>
namespace:</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>The last feature of the source of truth is the ability to use <a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a>
templates for keys and values by prefixing them with “<code>~</code>”:</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="c1"># In 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"># In 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>Looking up for <code>netbox</code> in the <code>system</code> namespace for
<code>to1-p2.ussfo03.blade-group.net</code> yields the following result:</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>This also works for structured data:</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="c1"># In 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"># In 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>This yields the following result:</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>When putting data in the source of truth, we use the following rules:</p>
<ol>
<li>Don’t repeat yourself.</li>
<li>Put the data in the most specific place without breaking the first rule.</li>
<li>Use templates with parsimony, mostly to help with the previous rules.</li>
<li>Restrict the data model to what is needed for your use case.</li>
</ol>
<p>The first rule is important. For example, when specifying IP addresses
for a point-to-point link, only specify one side and deduce the other
value in the templates. The last rule means you do not need to mimic a
<a href="https://github.com/openconfig/public/tree/master/release/models/bgp" title="OpenConfig's YANG model for BGP">BGP YANG model</a> to specify BGP peers and policies:</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="templates">Templates</h2>
<p>The list of templates to compile for each device is stored in the source
of truth, under the <code>build</code> namespace:</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>Templates are using <a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a>. This is the same engine used in
<em>Ansible</em>. <em>Jerikan</em> ships some custom filters but also reuses some of
the useful filters from <em>Ansible</em>, notably
<a href="https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_filters_ipaddr.html" title="Ansible: ipaddr filter"><code>ipaddr</code></a>. Here is an excerpt of
<a href="https://github.com/jerikan-network/cmdb/blob/main/templates/junos/base.j2"><code>templates/junos/base.j2</code></a> to configure DNS
and NTP servers on Juniper devices:</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>The equivalent template for Cisco IOS XR is:</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>There are three helper functions provided:</p>
<ul>
<li><code>devices()</code> returns the list of devices matching a set of
   conditions on the scope. For example, <code>devices("location==ussfo03",
   "groups==tor-bgp")</code> returns the list of devices in San Francisco in
   the <code>tor-bgp</code> group. You can also omit the operator if you want the
   specified value to be equal to the one in the local scope. For
   example, <code>devices("location")</code> returns devices in the current
   location.</li>
<li><code>lookup()</code> does a key lookup. It takes the namespace, the key, and
   optionally, a device name. If not provided, the current device
   is assumed.</li>
<li><code>scope()</code> returns the scope of the provided device.</li>
</ul>
<p>Here is how you would define iBGP sessions between edge devices in the
same location:</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>We also have a global key-value store to save information to be
reused in another template or device. This is quite useful to
automatically build DNS records. First, “capture” the IP address
inserted into a template with <code>store()</code> as a filter:</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>Then, reuse it later to build DNS records by iterating over <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>Templates are compiled locally with <code>./run-jerikan build</code>. The
<code>--limit</code> argument restricts the devices to generate configuration
files for. Build is not done in parallel because a template may depend
on the data collected by another template. Currently, it takes 1
minute to compile around 3000 files spanning over 800 devices.</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="Jerikan outputs when building&#10;templates" 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>Output of Jerikan after building configuration files for six devices</figcaption></figure>
<p>When an error occurs, a detailed traceback is displayed, including the
template name, the line number and the value of all visible variables.
This is a major time-saver compared to <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>We don’t have general-purpose rules when writing templates. Like for
the source of truth, there is no need to create generic templates able
to produce any BGP configuration. There is a balance to be found
between readability and avoiding duplication. Templates can become
scary and complex: sometimes, it’s better to write a filter or a
function in <a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jinja.py"><code>jerikan/jinja.py</code></a>. Mastering
<a href="https://jinja.palletsprojects.com/en/stable/templates/" title="Jinja2: moteur de modèles pour Python">Jinja2</a> is a good investment. Take time to browse through <a href="https://github.com/jerikan-network/cmdb/tree/main/templates">our
templates</a> as some of them show interesting features.</p>
<h2 id="checks">Checks</h2>
<p>Optionally, each configuration file can be validated by a script in
the <a href="https://github.com/jerikan-network/cmdb/tree/main/checks"><code>checks/</code></a> directory. <em>Jerikan</em> looks up the key <code>checks</code>
in the <code>build</code> namespace to know which checks to run:</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>In the above example, <code>checks/junoser</code> is executed if there is a
change to the generated <code>config.txt</code> file. It also outputs a
transformed version of the configuration file which is easier to
understand when using <code>diff</code>. <a href="https://github.com/codeout/junoser/" title="PEG parser for Junos configuration">Junoser</a> checks a Junos configuration
file using Juniper’s XML schema definition for Netconf.<sup id="fnref:fork"><a class="footnote-ref" href="#fn:fork">6</a></sup> On
error, <em>Jerikan</em> displays:</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-into-gitlab-ci">Integration into GitLab CI</h2>
<p>The next step is to compile the templates using a CI. As we are using
GitLab, <em>Jerikan</em> ships with a <a href="https://github.com/jerikan-network/cmdb/blob/main/.gitlab-ci.yml"><code>.gitlab-ci.yml</code></a>
file. When we need to make a change, we create a dedicated branch and
a merge request. GitLab compiles the templates using the same
environment we use on our laptops and stores them as an artifact.</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="Merge requests in GitLab for a&#10;change" 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>Merge request to add a new port in USSFO03. The templates were compiled successfully but approval from another team member is still required to merge.</figcaption></figure>
<p>Before approving the merge request, another team member looks at the
changes in data and templates but also the differences for the
generated configuration files:</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="Differences for generated configuration&#10;files" 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>The change configures a port on the Juniper device, adds records to DNS, and updates NetBox with the new IP addresses (not shown).</figcaption></figure>
<h1 id="ansible">Ansible</h1>
<p>After <em>Jerikan</em> has built the configuration files, <em>Ansible</em> takes
over. It is also packaged as a Docker image to avoid the trouble of
maintaining the right Python virtual environment and ensure everyone is
using the same versions.</p>
<h2 id="inventory">Inventory</h2>
<p><em>Jerikan</em> has generated an inventory file. It contains all the managed
devices, the variables defined for each of them and the groups
converted to <em>Ansible</em> groups:</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> is a special group for devices whose configuration should
match the golden configuration. Daily and unattended, <em>Ansible</em> should
be able to push configurations to this group. The mid-term goal is to
cover all devices.</p>
<p><code>none</code> is a special device for tasks not related to a specific host.
This includes <a href="/en/blog/2020-syncing-netbox-ansible" title="Syncing NetBox with a custom Ansible module">synchronizing NetBox</a>, <a href="/en/blog/2020-syncing-ripe-arin-apnic-ansible" title="Syncing RIPE, ARIN, and APNIC objects with a custom Ansible module"><abbr title="Internet Routing Registry">IRR</abbr> objects</a>, and the DNS,
updating the <abbr title="Resource Public Key Infrastructure">RPKI</abbr>, and building the geofeed files.</p>
<h2 id="playbook">Playbook</h2>
<p>We use a single playbook for all devices. It is described in the
<a href="https://github.com/jerikan-network/cmdb/blob/main/ansible/playbooks/site.yaml"><code>ansible/playbooks/site.yaml</code></a> file.
Here is a shortened version:</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>A host executes only one of the plays. For example, a Junos device
executes the <code>blade.junos</code> role. Once a play has been executed, the
device is added to the <code>done</code> group and the other plays are skipped.</p>
<p>The playbook can be executed with the configuration files generated by
the GitLab CI using the <code>./run-ansible-gitlab</code> command. This is a
wrapper around Docker and the <code>ansible-playbook</code> command and it
accepts the same arguments. To deploy the configuration on the edge
devices for the SK1 datacenter in “check” mode, we use:</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>We have some rules when writing roles:</p>
<ul>
<li><code>--check</code> must detect if a change is needed;</li>
<li><code>--diff</code> must provide a visualization of the planned changes;</li>
<li><code>--check</code> and <code>--diff</code> must not display anything if there is nothing to change;</li>
<li><a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">writing a custom module</a> tailored to our needs is a valid solution;</li>
<li>the whole device configuration is managed;<sup id="fnref:derivation"><a class="footnote-ref" href="#fn:derivation">7</a></sup></li>
<li>secrets must be stored in Vault;</li>
<li>templates should be avoided as we have <em>Jerikan</em> for that; and</li>
<li>avoid duplication and reuse tasks.<sup id="fnref:duplication"><a class="footnote-ref" href="#fn:duplication">8</a></sup></li>
</ul>
<p>We avoid using collections from <a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a>, the exception
being collections to connect and interact with vendor devices, like
<a href="https://galaxy.ansible.com/cisco/iosxr" title="Ansible Galaxy: cisco.iosxr collection"><code>cisco.iosxr</code> collection</a>. The quality of <em>Ansible
Galaxy</em> collections is quite random and it is an additional
maintenance burden. It seems better to write roles tailored to our
needs. The collections we use are in
<a href="https://github.com/jerikan-network/cmdb/blob/main/ci/ansible/ansible-galaxy.yaml"><code>ci/ansible/ansible-galaxy.yaml</code></a>. We use
<a href="https://mitogen.networkgenomics.com/ansible_detailed.html" title="Mitogen for Ansible">Mitogen</a> to get a 10× speedup on <em>Ansible</em> executions on Linux
hosts.</p>
<p>We also have a few playbooks for operational purposes: upgrading the OS
version, isolating an edge router, etc. We were also planning on how to
add operational checks in roles: are all the BGP sessions up? They
could have been used to validate a deployment—and rollback if there is
an issue.</p>
<p>Currently, our playbooks are run from our laptops. To keep tabs, we
are using <a href="https://ara.recordsansible.org" title="ARA Records Ansible">ARA</a>. A weekly dry-run on devices in the <code>in-sync</code> group
also provides a dashboard on which devices we need to run <em>Ansible</em>
on.</p>
<h1 id="configuration-data-and-templates">Configuration data and templates</h1>
<p><em>Jerikan</em> ships with pre-populated data and templates matching the
configuration of our USSFO03 and SK1 datacenters. They do not exist
anymore but, we promise, all this was used in production back in
the day! 😢</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="Network architecture for Blade&#10;datacenter" 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>The latest iteration of our network infrastructure for SK1, USSFO03, and future data centers. The production network is using BGPttH using a spine-leaf fabric. The out-of-band network is using a simple L2 design, using the spanning tree protocol, as well as a set of console servers.</figcaption></figure>
<p>Notably, you can find the configuration for:</p>
<dl>
<dt>Our edge routers</dt>
<dd>Some are running on <em>Junos</em>, like <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/edge2.ussfo03.blade-group.net/config.txt">edge2.ussfo03</a>, the others on
  <em>IOS XR</em>, like <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/edge1.sk1.blade-group.net/config.txt">edge1.sk1</a>. The implemented functionalities are
  similar in both cases and we could swap one for the other. It
  includes the BGP configuration for transits, peerings, and <abbr title="Internet Exchange">IX</abbr> as
  well as the associated BGP policies. <a href="https://www.peeringdb.com/" title="The Interconnection Database">PeeringDB</a> is queried to get
  the maximum number of prefixes to accept for peerings. <a href="/en/blog/2020-bgpq4-irrd-docker" title="Speeding up bgpq4 with IRRd in a container">bgpq3 and a
  containerized IRRd</a> help to filter received routes. A firewall is
  added to protect the routing engine. Both IPv4 and IPv6 are
  configured.</dd>
<dt>Our BGP-based fabric</dt>
<dd>BGP is used inside the datacenter<sup id="fnref:evpn"><a class="footnote-ref" href="#fn:evpn">9</a></sup> and is extended on
  bare-metal hosts. The configuration is automatically derived from
  the device location and the port number.<sup id="fnref:bgptth"><a class="footnote-ref" href="#fn:bgptth">10</a></sup> Top-of-the-rack
  devices are using passive BGP sessions for ports towards servers.
  They are also serving a provisioning network to let them boot using
  DHCP and PXE. They also act as a DHCP server. The design is
  multivendor. Some devices are running <em>Cumulus Linux</em>, like
  <a href="https://github.com/jerikan-network/cmdb/tree/generated-public/output/to1-p1.ussfo03.blade-group.net">to1-p1.ussfo03</a>, while some others are running <em>Junos</em>, like
  <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>Our out-of-band fabric</dt>
<dd>We are using Cisco Catalyst 2960 switches to build an L2 out-of-band
  network. To provide redundancy and save a few bucks on wiring, we
  build small loops and run the spanning-tree protocol. See
  <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/ob1-p1.ussfo03.blade-group.net/config.txt">ob1-p1.ussfo03</a>. It is redundantly connected to our gateway
  servers. We also use OpenGear devices for console access. See
  <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>Our administrative gateways</dt>
<dd>These Linux servers have multiple purposes: SSH jump boxes, rescue
  connection, direct access to the out-of-band network, zero-touch
  provisioning of network devices,<sup id="fnref:ztp"><a class="footnote-ref" href="#fn:ztp">11</a></sup> Internet access for management
  flows, centralization of the console servers using <a href="https://conserver.com/">Conserver</a>,
  and API for autoconfiguration of BGP sessions for bare-metal
  servers. They are the first servers installed in a new datacenter
  and are used to provision everything else. Check both the <a href="https://github.com/jerikan-network/cmdb/tree/generated-public/output/gateway1.ussfo03.blade-group.net">generated
  files</a> and the associated <a href="https://github.com/jerikan-network/cmdb/tree/main/ansible/roles/blade.adm-gateway/tasks">Ansible tasks</a>.</dd>
</dl>
<div class="admonition">
<p class="admonition-title">Update (2021-10)</p>
<p>I gave a talk about Jerikan and Ansible during
<a href="/en/blog/2021-frnog34-jerikan">FRnOG #34</a>. It is essentially
the same as this article.</p>
</div>
<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>This post was briefly discussed on <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> does not even provide a line number when there is an
error in a template. You may need to find the problem by
bisecting.</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>You may recognize the same concepts as in <a href="https://help.puppet.com/core/current/Content/PuppetCore/hiera_intro.htm">Hiera</a>, the
hierarchical key-value store from <em>Puppet</em>. At first, we were
using <a href="https://web.archive.org/web/20210520014417/http://jerakia.io/" title="Jerakia: a versatile data lookup system">Jerakia</a>, a similar independent store exposing an HTTP
REST interface. However, the lookup overhead is too large for our
use. <em>Jerikan</em> implements the same functionality within a <a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jerakia.py">Python
function</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>This file could be extended to specify a schema to validate
the source of truth and detect errors earlier. It could also
provide default values to keep templates from interpreting
incomplete structures. <a href="https://github.com/23andMe/Yamale" title="A schema and validator for YAML">Yamale</a> could be a starting point for
such a feature. <a class="footnote-backref" href="#fnref:schema" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:extend">
<p>The list of available filters is mangled inside
<a href="https://github.com/jerikan-network/cmdb/blob/main/jerikan/jinja.py"><code>jerikan/jinja.py</code></a>. This is a remnant of the
fact we do not maintain <em>Jerikan</em> as a standalone software. <a class="footnote-backref" href="#fnref:extend" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:function">
<p>This is a bit confusing: we have a <code>store()</code> filter and a
<code>store()</code> function. With <em>Jinja2</em>, filters and functions live in
two different namespaces. <a class="footnote-backref" href="#fnref:function" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:fork">
<p>We are using a <a href="https://github.com/jerikan-network/junoser" title="Junoser fork">fork</a> with some modifications to be able to
validate our configurations and exposing an HTTP service to
reduce the time spent on each configuration check. <a class="footnote-backref" href="#fnref:fork" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:derivation">
<p>There is a trend in network automation to automate a
configuration subset, for example by having a playbook to create a
new BGP session. We believe this is wrong: with time, your
configuration will get out-of-sync with its expected state,
notably hand-made changes will be left undetected. <a class="footnote-backref" href="#fnref:derivation" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:duplication">
<p>See
<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>
and
<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>.
They are meant to be called when needed, using <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>We also have some datacenters using BGP EVPN VXLAN at
medium-scale using Juniper devices. As they are still in
production today, we didn’t include this feature but we may
publish it in the future. <a class="footnote-backref" href="#fnref:evpn" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:bgptth">
<p>In retrospect, this may not be a good idea unless you are
pretty sure everything is uniform (number of switches for each
layer, number of ports). This was not our case. We now think it is
a better idea to assign a prefix to each device and write it in
the source of truth. <a class="footnote-backref" href="#fnref:bgptth" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:ztp">
<p>Non-linux based devices are upgraded and configured
unattended. Cumulus Linux devices are automatically upgraded on
install but the final configuration has to be pushed using
<em>Ansible</em>: we didn’t want to duplicate the configuration process
using another tool. <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">Syncing RIPE, ARIN, and APNIC objects with a custom Ansible module</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2020-syncing-ripe-arin-apnic-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2020-syncing-ripe-arin-apnic-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2020-09-28T06:33:30Z</updated>
    <id>http://www.luffy.cx/en/blog/2020-syncing-ripe-arin-apnic-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>The Internet is split into five <a href="https://en.wikipedia.org/wiki/Regional_Internet_registry" title="Regional Internet registry on Wikipedia">regional Internet registry</a>: <abbr title="African Network Information Center">AFRINIC</abbr>,
<abbr title="American Registry for Internet Numbers">ARIN</abbr>, <abbr title="Asia-Pacific Network Information Center">APNIC</abbr>, <abbr title="Latin America and Caribbean Network Information Center">LACNIC</abbr>, and <abbr title="Réseaux IP Européens Network Coordination Center">RIPE</abbr>. Each <abbr title="Regional Internet Registry">RIR</abbr> maintains an <a href="https://en.wikipedia.org/wiki/Internet_Routing_Registry" title="Internet Routing Registry on Wikipedia">Internet Routing
Registry</a>. An <abbr title="Internet Routing Registry">IRR</abbr> allows one to publish <strong>information about the
routing of Internet number resources</strong>.<sup id="fnref:others"><a class="footnote-ref" href="#fn:others">1</a></sup> Operators use this to
determine the owner of an IP address and to construct and maintain
<strong>routing filters</strong>. To ensure your routes are widely accepted, it is
important to keep the prefixes you announce up-to-date in an <abbr title="Internet Routing Registry">IRR</abbr>.</p>
<p>There are two common tools to query this database: <code>whois</code> and
<code>bgpq4</code>. The first one allows you to do a query with the <a href="https://www.rfc-editor.org/rfc/rfc3912" title="RFC 3912: WHOIS Protocol Specification">WHOIS
protocol</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>The second one allows you to build route filters using the information
contained in the <abbr title="Internet Routing Registry">IRR</abbr> database:</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>There is no module available on <a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a> to manage these
objects. Each <abbr title="Internet Routing Registry">IRR</abbr> has different ways of being updated. Some <abbr title="Regional Internet Registries">RIRs</abbr>
propose an API, but some don’t. If we restrict ourselves to <abbr title="Réseaux IP Européens Network Coordination Center">RIPE</abbr>, <abbr title="American Registry for Internet Numbers">ARIN</abbr>,
and <abbr title="Asia-Pacific Network Information Center">APNIC</abbr>, the only common method to update objects is <a href="https://docs.db.ripe.net/Update-Methods/Email-Updates" title="RIPE NCC: Email Updates">email
updates</a>, authenticated with a password or a PGP signature.<sup id="fnref:support"><a class="footnote-ref" href="#fn:support">2</a></sup>
Let’s write a custom Ansible module for this purpose!</p>
<div class="admonition">
<p class="admonition-title">Notice</p>
<p>I recommend that you read “<a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">Writing a custom Ansible
module</a>” as an introduction, as well as “<a href="/en/blog/2020-syncing-mysql-tables-ansible" title="Syncing MySQL tables with a custom Ansible module">Syncing MySQL tables</a>”
for a more instructive example.</p>
</div>
<div class="toc">
<ul>
<li><a href="#code">Code</a><ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#module-definition">Module definition</a></li>
<li><a href="#getting-existing-objects">Getting existing objects</a></li>
<li><a href="#comparing-with-wanted-objects">Comparing with wanted objects</a></li>
<li><a href="#returning-updates">Returning updates</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="code">Code</h1>
<p>The <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">module</a> takes a list of <abbr title="Routing Policy Specification Language">RPSL</abbr> objects to synchronize and returns
the body of an email update if a change is needed:</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="prerequisites">Prerequisites</h2>
<p>The source file should be a set of objects to sync using the <a href="https://www.rfc-editor.org/rfc/rfc2622" title="RFC 2622: Routing Policy Specification Language (RPSL)"><abbr title="Routing Policy Specification Language">RPSL</abbr>
language</a>. This would be the same content you would send manually by
email. All objects should be managed by the same maintainer, which is
also provided as a parameter.</p>
<p>Signing<sup id="fnref:arin"><a class="footnote-ref" href="#fn:arin">3</a></sup> and sending the result is not the responsibility of
this module. You need two additional tasks for this purpose:</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>You also need to authorize the PGP keys used to sign the updates by
creating a <code>key-cert</code> object and adding it as a valid authentication
method for the corresponding <code>mntner</code> object:</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="module-definition">Module definition</h2>
<p>Starting from the skeleton described in the <a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">previous article</a>, we define the 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="getting-existing-objects">Getting existing objects</h2>
<p>To grab existing objects, we use the <code>whois</code> command to retrieve all
the objects from the provided maintainer.</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>The first part of the code sets up some <abbr title="Internet Routing Registry">IRR</abbr>-specific constants: the
server to query, the options to provide to the <code>whois</code> command and the
objects to exclude from synchronization. The second part invokes the
<code>whois</code> command, requesting all objects whose <code>mnt-by</code> field is the
provided maintainer. Here is an example of output:</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>The result is passed to the <code>extract()</code> function. It parses and
normalizes the results into a dictionary mapping object names to
objects. We store the result in the <code>got</code> variable.</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> is a class enabling normalization and comparison of
objects. Look at the <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">module code</a> for more details.</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="comparing-with-wanted-objects">Comparing with wanted objects</h2>
<p>Let’s build the <code>wanted</code> dictionary using the same structure, thanks
to the <code>extract()</code> function we can use verbatim:</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>The next step is to compare <code>got</code> and <code>wanted</code> to build the diff
object:</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="returning-updates">Returning updates</h2>
<p>The module does not have side effects. If there is a difference, we
return the updates to send by email. We choose to include all wanted
objects in the updates (contained in the <code>source</code> variable) and let
the <abbr title="Internet Routing Registry">IRR</abbr> ignore unmodified objects. We also append the objects to be
deleted by adding a <code>delete:</code> attribute to each of them.</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>The complete code is available on <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/irr_sync.py">GitHub</a>. The module
supports both <code>--diff</code> and <code>--check</code> flags. It does not return
anything if no change is detected. It can work with <abbr title="Asia-Pacific Network Information Center">APNIC</abbr>, <abbr title="Réseaux IP Européens Network Coordination Center">RIPE</abbr>, and
<abbr title="American Registry for Internet Numbers">ARIN</abbr>. It is not perfect: it may not detect some changes,<sup id="fnref:changes"><a class="footnote-ref" href="#fn:changes">4</a></sup> it
is not able to modify objects not owned by the provided
maintainer<sup id="fnref:mntner"><a class="footnote-ref" href="#fn:mntner">5</a></sup> and some attributes cannot be modified, requiring you
to manually delete and recreate the updated object.<sup id="fnref:modify"><a class="footnote-ref" href="#fn:modify">6</a></sup> However,
this module should automate 95% of your needs.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:others">
<p>Other <abbr title="Internet Routing Registries">IRRs</abbr> exist without being attached to an <abbr title="Regional Internet Registry">RIR</abbr>. The most
notable one is <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><abbr title="American Registry for Internet Numbers">ARIN</abbr> is phasing out this method in favor of <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>.
Moreover, PGP signatures were never supported outside the test
environment. <abbr title="Réseaux IP Européens Network Coordination Center">RIPE</abbr> has an <a href="https://docs.db.ripe.net/Update-Methods/RESTful-API" title="RIPE Database RESTful API">API</a> available, but email
updates are still supported and not planned to be deprecated.
<abbr title="Asia-Pacific Network Information Center">APNIC</abbr> plans to expose an <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 is not supported by <abbr title="American Registry for Internet Numbers">ARIN</abbr> outside the test environment.
You need to use the password method instead… 😕 <a class="footnote-backref" href="#fnref:arin" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:changes">
<p>For <abbr title="American Registry for Internet Numbers">ARIN</abbr>, we cannot query <code>key-cert</code> and <code>mntner</code> objects
and therefore we cannot detect changes in them. It is also not
possible to detect changes to the auth mechanisms of a <code>mntner</code>
object. <a class="footnote-backref" href="#fnref:changes" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:mntner">
<p><abbr title="Asia-Pacific Network Information Center">APNIC</abbr> does not assign top-level objects to the maintainer
associated with the owner. <a class="footnote-backref" href="#fnref:mntner" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:modify">
<p>Changing the status of an <code>inetnum</code> object requires
deleting and recreating the object. <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">Syncing NetBox with a custom Ansible module</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2020-syncing-netbox-ansible" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2020-syncing-netbox-ansible#isso-thread" rel="replies" type="text/html"/>
    <updated>2020-09-19T14:11:34Z</updated>
    <id>http://www.luffy.cx/en/blog/2020-syncing-netbox-ansible.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>The <a href="https://netbox-ansible-collection.readthedocs.io/en/latest/" title="netbox.netbox Ansible Collection's documentation"><code>netbox.netbox</code> collection</a> from <a href="https://galaxy.ansible.com" title="Ansible Galaxy">Ansible Galaxy</a>
provides several modules to update <a href="https://netbox.readthedocs.io/en/stable/" title="NetBox documentation">NetBox</a> objects:</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>However, if <em>NetBox</em> is <strong>not your source of truth</strong>, you may want to
ensure it stays in sync with your configuration management
database<sup id="fnref:cmdb"><a class="footnote-ref" href="#fn:cmdb">1</a></sup> by removing outdated devices or IP addresses. While it
should be possible to glue together a playbook with a query, a loop,
and some filtering to delete unwanted elements, it feels clunky,
inefficient, and an abuse of YAML as a programming language. A specific
Ansible module solves this issue and is likely more flexible.</p>
<div class="toc">
<ul>
<li><a href="#code">Code</a><ul>
<li><a href="#module-definition">Module definition</a></li>
<li><a href="#abstracting-synchronization">Abstracting synchronization</a><ul>
<li><a href="#synchronizing-tags-and-tenants">Synchronizing tags and tenants</a></li>
<li><a href="#synchronizing-sites">Synchronizing sites</a></li>
<li><a href="#synchronizing-manufacturers-device-types-and-device-roles">Synchronizing manufacturers, device types, and device roles</a></li>
<li><a href="#synchronizing-devices">Synchronizing devices</a></li>
<li><a href="#synchronizing-ip-addresses">Synchronizing IP addresses</a></li>
</ul>
</li>
<li><a href="#getting-the-current-and-wanted-states">Getting the current and wanted states</a></li>
<li><a href="#applying-changes">Applying changes</a></li>
</ul>
</li>
</ul>
</div>
<div class="admonition">
<p class="admonition-title">Notice</p>
<p>I recommend that you read “<a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">Writing a custom Ansible
module</a>” as an introduction, as well as “<a href="/en/blog/2020-syncing-mysql-tables-ansible" title="Syncing MySQL tables with a custom Ansible module">Syncing MySQL tables</a>”
for a first simpler example.</p>
</div>
<h1 id="code">Code</h1>
<p>The <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">module</a> has the following signature and it syncs <em>NetBox</em> with
the content of the provided YAML file:</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>The synchronized objects are:</p>
<ul>
<li>sites</li>
<li>manufacturers</li>
<li>device types</li>
<li>device roles</li>
<li>devices</li>
<li>IP addresses</li>
</ul>
<p>In our environment, the YAML file is generated from our configuration
management database and contains a set of devices and a list of IP
addresses:</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>The network team is not the sole tenant in <em>NetBox</em>. While adding new
objects or modifying existing ones should be relatively safe, deleting
unwanted objects can be risky. The module only deletes objects it did
create or modify. To identify them, it marks them with a specific tag,
<code>cmdb</code>. Most objects in <em>NetBox</em> accept tags.</p>
<h2 id="module-definition">Module definition</h2>
<p>Starting from the skeleton described in the <a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">previous article</a>, we define the 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>It contains additional optional arguments defining the number of
workers to talk to <em>NetBox</em> and query the existing objects in parallel
to speed up the execution.</p>
<h2 id="abstracting-synchronization">Abstracting synchronization</h2>
<p>We need to synchronize different object types, but once we have a list
of objects we want in <em>NetBox</em>, the grunt work is always the same:</p>
<ul>
<li>check if the objects already exist,</li>
<li>retrieve them and put them in a form suitable for comparison,</li>
<li>retrieve the extra objects we don’t want anymore,</li>
<li>compare the two sets, and</li>
<li>add missing objects, update existing ones, delete extra ones.</li>
</ul>
<p>We code these behaviors into a <code>Synchronizer</code> abstract class. For each
kind of object, a concrete class is built with the appropriate <a href="https://www.attrs.org/en/stable/index.html" title="attrs: Classes Without Boilerplate">class
attributes</a> to tune its behavior and a <code>wanted()</code> method to provide
the objects we want.</p>
<p>I am not explaining the abstract class code here. Have a look at the
<a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">source</a> if you want.</p>
<h3 id="synchronizing-tags-and-tenants">Synchronizing tags and tenants</h3>
<p>As a starter, here is how we define the class synchronizing the tags:</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>The <code>app</code> and <code>table</code> attributes define the <em>NetBox</em> objects we want
to manipulate. The <code>key</code> attribute is used to determine how to look up
existing objects. In this example, we want to look up tags using
their names.</p>
<p>The <code>wanted()</code> method is expected to return a dictionary mapping
object keys to the list of wanted attributes. Here, the keys are tag
names and we create only one tag, <code>cmdb</code>, with the provided slug,
color, and description. This is the tag we will use to mark the objects
we create or modify.</p>
<p>If the tag does not exist, it is created. If it exists, the provided
attributes are updated. Other attributes are left untouched.</p>
<p>We also want to create a specific tenant for objects accepting such an
attribute (devices and IP addresses):</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="synchronizing-sites">Synchronizing sites</h3>
<p>We also need to synchronize the list of sites. This time, the
<code>wanted()</code> method uses the information provided in the YAML file: it
walks the devices and builds a set of datacenter names.</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>Thanks to the use of the <code>only_on_create</code> attribute, the specified
attributes are not updated if they are different. The goal of this
synchronizer is mostly to collect the references to the different
sites for other objects.</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="synchronizing-manufacturers-device-types-and-device-roles">Synchronizing manufacturers, device types, and device roles</h3>
<p>The synchronization of manufacturers is pretty similar, except we do
not use the <code>only_on_create</code> attribute:</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>Regarding the device types, we use the <code>foreign</code> attribute linking
a <em>NetBox</em> attribute to the synchronizer handling it.</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>The <code>wanted()</code> method refers to the manufacturer using its <code>key</code>
attribute. In this case, this is the manufacturer name.</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>The device roles are defined like this:</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="synchronizing-devices">Synchronizing devices</h3>
<p>A device is mostly a name with references to a role, a model, a
datacenter and a tenant. These references are declared as foreign keys
using the synchronizers defined previously.</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>The <code>remove_unused</code> attribute is a safety implemented to fail if we
have to delete more than 10 devices: this may be the indication there
is a bug somewhere, unless one of your datacenters suddenly caught
fire.</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="synchronizing-ip-addresses">Synchronizing IP addresses</h3>
<p>The last step is to synchronize IP addresses. We do not attach them to
a device.<sup id="fnref:devices"><a class="footnote-ref" href="#fn:devices">2</a></sup> Instead, we specify the device names in the
description of the IP address:</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>There is a slight difficulty: <em>NetBox</em> allows duplicate IP addresses,
so a simple lookup is not enough. In case of multiple matches, we
choose the best by preferring those tagged with <code>cmdb</code>, then those
already attached to an 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="getting-the-current-and-wanted-states">Getting the current and wanted states</h2>
<p>Each synchronizer is initialized with a reference to the Ansible
module, a reference to a <a href="https://pynetbox.readthedocs.io/en/latest/">pynetbox</a>’s API object, the data contained
in the provided YAML file and two empty dictionaries for the current
and expected states:</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>Each synchronizer has a <code>prepare()</code> method whose goal is to compute
the current and wanted states. It returns <code>True</code> in case of a
difference:</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="applying-changes">Applying changes</h2>
<p>Back to the skeleton described in the <a href="/en/blog/2020-custom-ansible-module" title="Writing a custom Ansible module">previous article</a>, the last step is to apply the changes if there
is a difference between these states. Each synchronizer registers the
current and wanted states in <code>sync_args["before"][table]</code> and
<code>sync_args["after"][table]</code> where <code>table</code> is the name of the table for
a given <em>NetBox</em> object type. The <code>diff</code> object is a bit elaborate as
it is built table by table. This enables <em>Ansible</em> to display the name
of each table before the diff representation:</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>Each synchronizer also exposes a <code>synchronize()</code> method to apply
changes and a <code>cleanup()</code> method to delete unwanted objects. Order is
important due to the relation between the objects.</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>The complete code is available on <a href="https://github.com/vincentbernat/ansible-custom-module-examples/blob/main/netbox_sync.py">GitHub</a>. Compared to using
<a href="https://netbox-ansible-collection.readthedocs.io/en/latest/" title="netbox.netbox Ansible Collection's documentation"><code>netbox.netbox</code> collection</a>, the logic is written in
Python instead of trying to glue Ansible tasks together. I believe
this is both more flexible and easier to read, notably when trying to
delete outdated objects. While I did not test it, it should also be
faster.</p>
<div class="admonition">
<p class="admonition-title">Update (2020-12)</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>”
shows how to update NetBox using the <code>netbox.netbox</code> collection. You
may compare the code with the solution I propose. The main difference
between the two is the cleaning of outdated objects in NetBox, which
is absent from the former solution.</p>
</div>
<p>An alternative would have been to reuse code from the <code>netbox.netbox</code>
collection, as it contains similar primitives. Unfortunately, I didn’t
think of it until now. 😶</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:cmdb">
<p>In my opinion, a good option for a <em>source of truth</em> is to
use YAML files in a <em>Git</em> repository. You get versioning for free
and people can get started with a text editor. <a class="footnote-backref" href="#fnref:cmdb" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:devices">
<p>This limitation is mostly due to laziness: we do not care
about this information. Our main motivation for putting IP
addresses in <em>NetBox</em> is to keep track of the used IP addresses.
However, if an IP address is already attached to an interface, we
leave this association untouched. <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>