<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[TTL255 - Przemek Rogala's blog]]></title><description><![CDATA[Computer Networks, Python and Automation]]></description><link>https://ttl255.com/</link><image><url>http://ttl255.com/favicon.png</url><title>TTL255 - Przemek Rogala&apos;s blog</title><link>https://ttl255.com/</link></image><generator>Ghost 1.24</generator><lastBuildDate>Sat, 04 Apr 2026 15:40:05 GMT</lastBuildDate><atom:link href="https://ttl255.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Developing NetBox Plugin - Part 5 - Permissions and API]]></title><description><![CDATA[Learn how to build your own NetBox plugins. In the final post of this tutorial we are implementing permissions and API.]]></description><link>https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/</link><guid isPermaLink="false">6045091bb430486ddc56ecfe</guid><category><![CDATA[netbox]]></category><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[tools]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 07 Mar 2021 19:43:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>In previous installments of this series we built out a fully functional plugin dedicated to tracking Bgp Peering connections. In this post we'll add final components: object permissions and API views.</p>
<h2 id="developingnetboxplugintutorialseries">Developing NetBox Plugin tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/">Developing NetBox Plugin - Part 1 - Setup and initial build</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/">Developing NetBox Plugin - Part 2 - Adding web UI pages</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-3-adding-search/">Developing NetBox Plugin - Part 3 - Adding search panel</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/">Developing NetBox Plugin - Part 4 - Small improvements</a></li>
<li>Developing NetBox Plugin - Part 5 - Permissions and API</li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#adding-permissio">Adding permissions</a>
<ul>
<li><a href="#adding-permissio">Adding permissions to views</a></li>
<li><a href="#adding-permissio">Adding permissions to Web GUI elements</a></li>
</ul>
</li>
<li><a href="#adding-api">Adding API</a>
<ul>
<li><a href="#build-serializer">Building serializer for BgpPeering objects</a></li>
<li><a href="#build-api-views">Building API views</a></li>
<li><a href="#define-url-for-a">Defininig URL for API calls</a></li>
</ul>
</li>
<li><a href="#api-usage-exampl">API usage examples</a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#resources">Resources</a></li>
<li><a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering" target="_blank">BGP Peering Plugin repository</a></li>
</ul>
<p><a name="adding-permissio"></a></p>
<h2 id="addingpermissions">Adding permissions</h2>
<p>Right now all users can view, edit and delete Bgp Peering objects. In the production system we would like to be able to have more granular control over who can perform a given operation. This is where the permissions system comes in.</p>
<p>In our plugin we will leverage Django authentication system <sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> to enable permissions for views we built out.</p>
<p><a name="adding-permissio"></a></p>
<h3 id="addingpermissionstoviews">Adding permissions to views</h3>
<p>Below are the changes I made to <code>views.py</code> to add permission handling to each of the views:</p>
<pre><code># views.py
from django.contrib.auth.mixins import PermissionRequiredMixin

...

class BgpPeeringView(PermissionRequiredMixin, View):
    &quot;&quot;&quot;Display BGP Peering details&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.view_bgppeering&quot;
    ...

class BgpPeeringListView(PermissionRequiredMixin, View):
    &quot;&quot;&quot;View for listing all existing BGP Peerings.&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.view_bgppeering&quot;
    ...

class BgpPeeringCreateView(PermissionRequiredMixin, CreateView):
    &quot;&quot;&quot;View for creating a new BgpPeering instance.&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.add_bgppeering&quot;
    ...

class BgpPeeringDeleteView(PermissionRequiredMixin, DeleteView):
    &quot;&quot;&quot;View for deleting a BgpPeering instance.&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.delete_bgppeering&quot;
    ...

class BgpPeeringEditView(PermissionRequiredMixin, UpdateView):
    &quot;&quot;&quot;View for editing a BgpPeering instance.&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.change_bgppeering&quot;
    ...
</code></pre>
<p>I've only included bits that have changed. Below is quick breakdown of what we did:</p>
<ul>
<li>
<p>We import <code>PermissionRequiredMixin</code> mixin class from <code>django.contrib.auth.mixins</code>. This class will handle permission checks logic and will plug into the NetBox's existing authorization system.</p>
</li>
<li>
<p>For each view class we add <code>PermissionRequiredMixin</code> to classes we subclass from. E.g.</p>
</li>
</ul>
<pre><code>class BgpPeeringCreateView(PermissionRequiredMixin, CreateView)
</code></pre>
<ul>
<li>In each of the classes we enabled for permission checks we need to specify permission, or iterable of permissions, required to access the view. To do that we use parameter <code>permission_required</code>. E.g.</li>
</ul>
<pre><code>class BgpPeeringDeleteView(PermissionRequiredMixin, DeleteView):
    &quot;&quot;&quot;View for deleting a BgpPeering instance.&quot;&quot;&quot;

    permission_required = &quot;netbox_bgppeering.delete_bgppeering&quot;
</code></pre>
<p>Permission names follow the following naming convention:</p>
<p><code>&lt;app_name&gt;.{view|add|delete|change}_&lt;model_name&gt;</code></p>
<p>Example permission names:</p>
<ul>
<li>
<p>Allow displaying BgpPeering instance: <code>netbox_bgppeering.view_bgppeering</code>.</p>
</li>
<li>
<p>Allow deleting BgpPeering instance: <code>netbox_bgppeering.delete_bgppeering</code>.</p>
</li>
<li>
<p>Allow modifying BgpPeering instance: <code>netbox_bgppeering.change_bgppeering</code>.</p>
</li>
</ul>
<p>After reloading NetBox try logging in as a non-privileged user and accessing BgpPeering plugin. You should get Access Denied message, like the one below:</p>
<p><img src="https://ttl255.com/content/images/2021/03/Clipboard_2021-02-28-15-05-27.png" alt="Clipboard_2021-02-28-15-05-27"></p>
<p>You can now switch to admin user and log into admin panel. In the admin panel if you add new permissions in <code>User &gt; Permissions</code>, you should see that <code>netbox_bgppeering &gt; bgp peering</code> permission now appears on the list of available object types.</p>
<p><img src="https://ttl255.com/content/images/2021/03/Screenshot_2021-02-28-Add-permission-NetBox.png" alt="Screenshot_2021-02-28-Add-permission-NetBox"></p>
<p><a name="adding-permissio"></a></p>
<h3 id="addingpermissionstowebguielements">Adding permissions to Web GUI elements</h3>
<p>We now have in place permissions that control the ability to view, add, delete, and edit Bgp Peering objects. But Web GUI elements, like the edit button, will still be shown to all users. Time to change that and make this behaviour permission dependent.</p>
<ol>
<li>First we set permission required to see plus, <code>+</code>, button in the top plugin menu. We edit <code>navigation.py</code>:</li>
</ol>
<pre><code class="language-python"># navigation.py
...
menu_items = (
    PluginMenuItem(
        ...
        buttons=(
            PluginMenuButton(
                ...
                permissions=[&quot;netbox_bgppeering.add_bgppeering&quot;],
            ),
</code></pre>
<p>There's only one addition. We pass a new parameter, <code>permissions</code>, when creating a <code>PluginMenuButton</code> object. We set its value to an iterable of permissions, in our case it's just one permission: <code>netbox_bgppeering.add_bgppeering</code>.</p>
<p>After this is done only users who have <code>Can add</code> permission on <code>BgpPeering</code> objects will be able to see the <code>+</code> button.</p>
<ol start="2">
<li>The final changes are to the templates that show <code>BgpPeering</code> related buttons. In our case those are <code>bgppeering.html</code> and <code>bgppeering_list.html</code>.</li>
</ol>
<pre><code class="language-html">&lt;!-- bgppeering.html --&gt;
...
&lt;div class=&quot;col-sm-8&quot;&gt;
    &lt;div class=&quot;pull-right noprint&quot;&gt;
        {% if perms.netbox_bgppeering.change_bgppeering %}
        &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_edit' pk=bgppeering.pk %}&quot; class=&quot;btn btn-warning&quot;&gt;
            &lt;span class=&quot;{{ icon_classes.pencil }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Edit
        &lt;/a&gt;
        {% endif %}
        {% if perms.netbox_bgppeering.delete_bgppeering %}
        &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_delete' pk=bgppeering.pk %}&quot; class=&quot;btn btn-danger&quot;&gt;
            &lt;span class=&quot;{{ icon_classes.trash }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Delete
        &lt;/a&gt;
        {% endif %}
    &lt;/div&gt;
&lt;/div&gt;
...
</code></pre>
<pre><code class="language-html">&lt;!-- bgppeering_list.html --&gt;
...
&lt;div class=&quot;pull-right noprint&quot;&gt;
    {% if perms.netbox_bgppeering.add_bgppeering %}
    &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_add' %}&quot; class=&quot;btn btn-primary&quot;&gt;
        &lt;span class=&quot;{{ icon_classes.plus }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Add
    &lt;/a&gt;
    {% endif %}
&lt;/div&gt;
...
</code></pre>
<p>To check permission is set before including the element we use <code>{% if perms.&lt;permission_name&gt; %}</code> conditional.</p>
<p>In our case we have the below conditionals:</p>
<ul>
<li><code>{% if perms.netbox_bgppeering.change_bgppeering %}</code> to include <code>Edit</code> button.</li>
<li><code>{% if perms.netbox_bgppeering.delete_bgppeering %}</code> to include <code>Delete</code> button.</li>
<li><code>{% if perms.netbox_bgppeering.bgppeering_add %}</code> to include <code>Add</code> button.</li>
</ul>
<p>If the user doesn't have one of those permissions then the corresponding button will not show in Web GUI.</p>
<p>And that's it. With changes to the views and Web GUI elements we have implemented a permissions system for our plugin.</p>
<p>Next stop, API!</p>
<p>Source code up to this point is in branch <code>adding-permission</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/adding-permissions" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/adding-permissions</a>.</p>
<p><a name="adding-api"></a></p>
<h2 id="addingapi">Adding API</h2>
<p>Being able to interact with BgpPeering objects in Web GUI is great and useful but it's not well suited to automation. If we want to programmatically interact with our plugin we need to expose it via API. And that's what we're going to do now.</p>
<p>To add code handling API we need to create a new directory called <code>api</code>. This is where source code files related to API functionality need to go.</p>
<p>In case of our plugin the path that includes <code>api</code> directory looks like so:</p>
<p><code>../ttl255-netbox-plugin-bgppeering/netbox_bgppeering/api</code></p>
<p>In this directory we will create three source code files, <code>serializers.py</code>, <code>urls.py</code> and <code>views.py</code>:</p>
<pre><code class="language-text">├── api
│   ├── serializers.py
│   ├── urls.py
│   └── views.py
</code></pre>
<p>With that out of the way, let's start coding!</p>
<p><a name="build-serializer"></a></p>
<h3 id="buildingserializerforbgppeeringobjects">Building serializer for BgpPeering objects</h3>
<p>First thing we need to do is to specify how BgpPeering model instances will be rendered into <code>json</code> representation.</p>
<p>For that purpose we will use Django REST framework <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> as well as existing NetBox serializers.</p>
<p>Our serializers go into the <code>api/serializers.py</code> file, and below is the serializer I wrote for the BgpPeering model.</p>
<pre><code class="language-python"># api/serializers.py

from rest_framework import serializers

from ipam.api.nested_serializers import (
    NestedIPAddressSerializer,
)
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer

from netbox_bgppeering.models import BgpPeering


class BgpPeeringSerializer(serializers.ModelSerializer):
    &quot;&quot;&quot;Serializer for the BgpPeering model.&quot;&quot;&quot;

    site = NestedSiteSerializer(
        many=False,
        read_only=False,
        required=False,
        help_text=&quot;BgpPeering Site&quot;,
    )

    device = NestedDeviceSerializer(
        many=False,
        read_only=False,
        required=True,
        help_text=&quot;BgpPeering Device&quot;,
    )

    local_ip = NestedIPAddressSerializer(
        many=False,
        read_only=False,
        required=True,
        help_text=&quot;Local peering IP&quot;,
    )

    class Meta:
        model = BgpPeering
        fields = [
            &quot;id&quot;,
            &quot;site&quot;,
            &quot;device&quot;,
            &quot;local_ip&quot;,
            &quot;local_as&quot;,
            &quot;remote_ip&quot;,
            &quot;remote_as&quot;,
            &quot;peer_name&quot;,
            &quot;description&quot;,
        ]
</code></pre>
<p>Few new concepts appear here so let's dig into this code.</p>
<ul>
<li>
<p><code>from rest_framework import serializers</code> - We import module <code>serializers</code> which contains base serializer class we will use.</p>
</li>
<li>
<p>Next we import nested serializers <sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup> for NetBox objects that we link to in our model. This will allow us to:</p>
<ul>
<li>Return <code>json</code> representation of those objects nested inside of BgpPeering data structure.</li>
<li>Use of <code>name</code>, <code>slug</code>, etc., fields for linked objects in POST/PATCH/PUT requests. Otherwise we'd only be allowed to pass <code>id</code> for these.</li>
</ul>
</li>
</ul>
<pre><code class="language-python"> from ipam.api.nested_serializers import (
    NestedIPAddressSerializer,
)
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
</code></pre>
<ul>
<li>Final import is the model that we're building a serializer for: <code>from netbox_bgppeering.models import BgpPeering</code>.</li>
</ul>
<p>With imports out of the way we can build the serializer class.</p>
<ul>
<li>
<p><code>BgpPeeringSerializer(serializers.ModelSerializer)</code> - I named our class <code>BgpPeeringSerializer</code> and I'm subclassing <code>ModelSerializer</code> class. That class will automatically handle serializing of most of the fields in our model, among other things.</p>
</li>
<li>
<p>In internal class <code>Meta</code> we specify a model that is being serialized, here <code>BgpPeering</code>. Then we specify a list of fields that we want to be included when serialization takes place. Fields that don't require special treatment will be automatically rendered thanks to the <code>BgpPeeringSerializer</code> class.</p>
</li>
</ul>
<pre><code class="language-python">    class Meta:
        model = BgpPeering
        fields = [
            &quot;id&quot;,
            &quot;site&quot;,
            &quot;device&quot;,
            &quot;local_ip&quot;,
            &quot;local_as&quot;,
            &quot;remote_ip&quot;,
            &quot;remote_as&quot;,
            &quot;peer_name&quot;,
            &quot;description&quot;,
        ]
</code></pre>
<ul>
<li>Finally, I'm telling the serializer class that three of the model fields need to be treated differently. Namely I want linked models to be also serialized and included in the <code>json</code> payload returned in the API response.</li>
</ul>
<p>This is not required but in a lot of cases it makes sense to return these data structures nested inside of the main structure. It will help us avoid multiple API calls that would be otherwise needed to retrieve details of linked objects.</p>
<p>It also simplifies adding/modifying objects via API, as already mentioned.</p>
<p>For each of the fields that need to contain nested data we must:</p>
<ul>
<li>Identify the nested serializer matching model this field links to.</li>
<li>Instantiate the class and assign the resulting object to the parameter named after the field.</li>
</ul>
<p>For example below is the nested serializer we use for <code>site</code> field:</p>
<pre><code>    site = NestedSiteSerializer(
        many=False,
        read_only=False,
        required=False,
        help_text=&quot;BgpPeering Site&quot;,
    )
</code></pre>
<p>When creating <code>NestedSiteSerializer</code> object we need to provide a few arguments <sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>:</p>
<ul>
<li><code>many</code> - set it to match the relationship type set on the field in the model. We don't have any many-to-many relationships so in our case <code>many</code> is set to <code>False</code>.</li>
<li><code>read_only</code> - set it to <code>True</code> if you want the field to be read-only and not allowed as the input in API calls. In our case all custom fields can be used as input so we set it to <code>False</code>.</li>
<li><code>required</code> - specifies whether field is required. This should follow the corresponding property we set for the model field.</li>
<li><code>help_text</code> - used to give this field description that is picked up when rendering the field.</li>
</ul>
<p>And that's it, our serializer is completed.</p>
<p><a name="build-api-views"></a></p>
<h3 id="buildingapiviews">Building API views</h3>
<p>With the serializer taken care of we move onto API view. This view goes into the <code>api/views.py</code> file and it will handle all of the different HTTP API calls.</p>
<pre><code class="language-python"># api/views.py

from rest_framework import mixins, viewsets

from netbox_bgppeering.models import BgpPeering
from netbox_bgppeering.filters import BgpPeeringFilter

from .serializers import BgpPeeringSerializer


class BgpPeeringView(
    mixins.CreateModelMixin,
    mixins.DestroyModelMixin,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    &quot;&quot;&quot;Create, check status of, update, and delete BgpPeering object.&quot;&quot;&quot;

    queryset = BgpPeering.objects.all()
    filterset_class = BgpPeeringFilter
    serializer_class = BgpPeeringSerializer
</code></pre>
<p>Not too much code but some new concepts, let's break it down:</p>
<ul>
<li><code>viewsets.GenericViewSet</code> - This is a <code>ViewSet</code> <sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup> class that lets us combine multiple different views into one view. Normally we would have to build separate views for adding, editing, deleting, etc. objects. We then would need to separately add each of these to URL conf. With <code>ViewSet</code> we need just one view and we will only need one entry in URL conf.</li>
</ul>
<p>Next, we subclass a number of mixin classes implementing different actions that will allow us to support HTTP request methods.</p>
<ul>
<li><code>mixins.CreateModelMixin</code> - This class takes care of creating and saving a new model instance. Responds to HTTP POST.</li>
<li><code>mixins.DestroyModelMixin</code> - This class will handle deletion of model instances. Responds to HTTP DELETE.</li>
<li><code>mixins.ListModelMixin</code> - This class allows returning a list of instances in API response. Used with HTTP GET.</li>
<li><code>mixins.RetrieveModelMixin</code> - This class handles retrieval of a single model instance. Used with HTTP GET.</li>
<li><code>mixins.UpdateModelMixin</code> - Finally, this class enables edits, both merge and replace. Used with HTTP PUT and PATCH.</li>
</ul>
<p>In the body of the class we have three attributes:</p>
<ul>
<li>
<p><code>queryset</code> - This is like in a normal view, we specify BgpPeering objects that are of interest. Here we want all objects to be up for grabs in API calls:</p>
<pre><code class="language-python">queryset = BgpPeering.objects.all()
</code></pre>
</li>
<li>
<p><code>filterset_class</code> - This specifies a filter class that can be used to apply search queries when retrieving objects via API. We're re-using <code>BgpPeeringFilter</code> which we built for the Search Panel. This means we can filter objects through API using the same queries we used in Web GUI.</p>
<pre><code class="language-python">filterset_class = BgpPeeringFilter
</code></pre>
<p>For example, if we wanted to get Bgp Peering objects that have string <code>primary</code> in description we could use general query parameter <code>q</code>:</p>
<p><code>http://localhost:8000/api/plugins/bgp-peering/bgppeering/?q=primary</code></p>
</li>
<li>
<p><code>serializer_class</code> - Finally we have a serializer class that we just built. This will be used to render our model into <code>json</code> representation.</p>
<pre><code class="language-python">serializer_class = BgpPeeringSerializer
</code></pre>
</li>
</ul>
<p><a name="define-url-for-a"></a></p>
<h3 id="definingurlforapicalls">Defining URL for API calls</h3>
<p>With serializer and view classes in place we just need to tie it together and expose API endpoints via URL. Code for that goes into the <code>api/urls.py</code> file.</p>
<pre><code># api/urls.py
from rest_framework import routers
from .views import BgpPeeringView

router = routers.DefaultRouter()

router.register(r&quot;bgppeering&quot;, BgpPeeringView)

urlpatterns = router.urls
</code></pre>
<p>This is slightly different to <code>urls.py</code> for Web GUI views. Here we use the router class <code>DefaultRouter</code> <sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup> which comes from Django REST Framework. This will automatically handle requests to API URLs exposed by API using different HTTP methods.</p>
<p>What this means is that we don't have to manually create multiple URL rules. Thanks to <code>DefaultRouter</code> we need only one:</p>
<pre><code class="language-python">router.register(r&quot;bgppeering&quot;, BgpPeeringView)
</code></pre>
<p>That's it. This one URL rule will handle GET, POST, etc. requests automatically.</p>
<p>Finally, we assign urls auto-generated by router class to <code>urlpatterns</code> variable. This variable is what Django uses for path mappings.</p>
<pre><code class="language-python">urlpatterns = router.urls
</code></pre>
<p>And we're done! With URLs in place we can take our API for a spin!</p>
<p><a name="api-usage-exampl"></a></p>
<h2 id="apiusageexamples">API usage examples</h2>
<p>If you now navigate to the main API URL for our plugin, <code>http://localhost:8000/api/plugins/bgp-peering/</code>, you should see the available endpoint.</p>
<p><img src="https://ttl255.com/content/images/2021/03/api_avail_endpoint.png" alt="api_avail_endpoint"></p>
<p>If we followed the URL for our endpoint we should get <code>json</code> reply with list of Bgp Peerings:</p>
<p><img src="https://ttl255.com/content/images/2021/03/api_list.png" alt="api_list"></p>
<p>And as I mentioned earlier we can use filtering as well, here we ask for peerings that have 'reindeer' string in the peer name or description.</p>
<p><img src="https://ttl255.com/content/images/2021/03/api_query.png" alt="api_query"></p>
<p>What's nice is that documentation for our API now shows up in Swagger docs:</p>
<p><img src="https://ttl255.com/content/images/2021/03/api_swagger.png" alt="api_swagger"></p>
<p>So we can view objects, but can we add or modify them?</p>
<p>Let's try out other HTTP methods using Postman.</p>
<p><strong>POST</strong>:<br>
<img src="https://ttl255.com/content/images/2021/03/post_w_site_small.png" alt="post_w_site_small"></p>
<p>Here we're adding a new object, notice nested payload values for site, device and IP address. If we didn't define nested serializers for these models we'd have to provide <code>id</code> of the objects we're linking to. This will make it much easier to consume our API for Bgp Peering plugin.</p>
<p>For completeness we'll try out PATCH and DELETE methods as well:</p>
<p><strong>PATCH:</strong><br>
<img src="https://ttl255.com/content/images/2021/03/patch_w_site_small.png" alt="patch_w_site_small"></p>
<p><strong>DELETE:</strong><br>
<img src="https://ttl255.com/content/images/2021/03/delete_w_site_small.png" alt="delete_w_site_small"></p>
<p>Awesome, everything is working as expected!</p>
<p>In case you were wondering, permissions we implemented in the first part of this post are automatically applied to API views.</p>
<p>This is what happens when user that has read-only access to Bgp Peering objects tries to create a new one:</p>
<p><img src="https://ttl255.com/content/images/2021/03/api_post_not_allowed_small.png" alt="api_post_not_allowed_small"></p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>And with that we came to the end of the tutorial walking you through the development of an example NetBox plugin. During our journey we built a functional plugin that implements a custom model representing Bgp Peering record. We built Web GUI views allowing users to view, add, edit and delete objects. Next we implemented object filtering before adding permissions and API.</p>
<p>There's much more we could do here. We could add export/import functionality. Perhaps improving data validation and search would be a welcome addition. But I believe that what you learned here should give you a solid base that you can build on top of. Everything else is up to your imagination.Happy coding!</p>
<p><a name="resources"></a></p>
<h2 id="resources">Resources</h2>
<ul>
<li>NetBox docs: Plugin Development: <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></li>
<li>NetBox source code on GitHub: <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a></li>
<li>NetBox Extensibility Overview, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=FSoCzuWOAE0" target="_blank">https://www.youtube.com/watch?v=FSoCzuWOAE0</a></li>
<li>NetBox Plugins Development, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=LUCUBPrTtJ4" target="_blank">https://www.youtube.com/watch?v=LUCUBPrTtJ4</a></li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Django authentication system: <a href="https://docs.djangoproject.com/en/3.1/topics/auth/default/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/auth/default/</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Django REST framework: <a href="https://www.django-rest-framework.org/" target="_blank">https://www.django-rest-framework.org/</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Django REST nested relationships: <a href="https://www.django-rest-framework.org/api-guide/relations/#nested-relationships" target="_blank">https://www.django-rest-framework.org/api-guide/relations/#nested-relationships</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Django REST serializer arguments: <a href="https://www.django-rest-framework.org/api-guide/fields/#core-arguments" target="_blank">https://www.django-rest-framework.org/api-guide/fields/#core-arguments</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Django REST ViewSets: <a href="https://www.django-rest-framework.org/api-guide/viewsets/" target="_blank">https://www.django-rest-framework.org/api-guide/viewsets/</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>Django REST DefaultRouter: <a href="https://www.django-rest-framework.org/api-guide/routers/#defaultrouter" target="_blank">https://www.django-rest-framework.org/api-guide/routers/#defaultrouter</a> <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Developing NetBox Plugin - Part 4 - Small improvements]]></title><description><![CDATA[Learn how to build your own NetBox plugins. In this post, we make many small improvements to the BgpPeering plugin.]]></description><link>https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/</link><guid isPermaLink="false">60205325b430486ddc56ecee</guid><category><![CDATA[netbox]]></category><category><![CDATA[python]]></category><category><![CDATA[automation]]></category><category><![CDATA[programming]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 07 Feb 2021 21:13:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Welcome to part 4 of the tutorial on developing NetBox plugin. By now BgpPeering plugin is functional but there are few things here and there that could make it better. In this post, we'll go through many improvements that will make the plugin look better and increase its functionality.</p>
<h2 id="developingnetboxplugintutorialseries">Developing NetBox Plugin tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/">Developing NetBox Plugin - Part 1 - Setup and initial build</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/">Developing NetBox Plugin - Part 2 - Adding web UI pages</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-3-adding-search/">Developing NetBox Plugin - Part 3 - Adding search panel</a></li>
<li>Developing NetBox Plugin - Part 4 - Small improvements</li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/">Developing NetBox Plugin - Part 5 - Permissions and API</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#updating-display">Updating display name of BgpPeering objects</a></li>
<li><a href="#enforce-the-same">Enforce the same network for local and remote IPs</a>
<ul>
<li><a href="#add-validation-t">Add validation to form class</a></li>
<li><a href="#show-errors-on-t">Show errors on the form in web UI</a></li>
</ul>
</li>
<li><a href="#allowing-objects">Allowing objects to be deleted</a>
<ul>
<li><a href="#creating-delete">Creating delete view</a></li>
<li><a href="#delete-confirmat">Delete confirmation template</a></li>
<li><a href="#add-url-to-delet">Add url to delete view</a></li>
<li><a href="#adding-object-vi">Adding object view header and delete button</a></li>
</ul>
</li>
<li><a href="#allowing-objects">Allowing objects to be edited</a>
<ul>
<li><a href="#creating-edit-vi">Creating edit view</a></li>
<li><a href="#modify-object-cr">Modify object creation template</a></li>
<li><a href="#adding-editing-b">Adding editing button and URL</a></li>
</ul>
</li>
<li><a href="#prettifying-fiel">Prettifying field labels</a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#resources">Resources</a></li>
<li><a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/small-improvements" target="_blank">BGP Peering Plugin GitHub repository</a></li>
</ul>
<p><a name="updating-display"></a></p>
<h2 id="updatingdisplaynameofbgppeeringobjects">Updating display name of BgpPeering objects</h2>
<p>We'll start improvements by changing default display name of BgpPeering objects.</p>
<p>Currently, when we create new objects we get a name that is not very descriptive:</p>
<p><img src="https://ttl255.com/content/images/2021/02/bgppeering_obj_old_name.png" alt="bgppeering_obj_old_name"></p>
<p>Instead of <code>BgpPeering object (28)</code> we'd like to have something more meaningful.</p>
<p>We could perhaps show here device name, peer name, or even remote BGP AS number.</p>
<p>Unfortunately, we decided to make peer name optional so this might not be a good candidate for the display name of an object. It's possible to include it conditionally. But that would result in inconsistent naming for objects that don't have peer name defined. For the sake of consistency, I will only include the device name and the remote BGP AS number.</p>
<p>Essentially, I want our display name to look like so:</p>
<pre><code>rtr-core-tokyo-02:3131
</code></pre>
<p>To do that we need to override <code>__str__</code> method in <code>BgpPeering</code> model class contained in <code>models.py</code>.</p>
<pre><code># models.py
class BgpPeering(ChangeLoggedModel):
...
    def __str__(self):
        return f&quot;{self.peer_name}:{self.remote_as}&quot;
</code></pre>
<p>If we now create a new object we'll see the below in changelog:</p>
<p><img src="https://ttl255.com/content/images/2021/02/bgppeering_obj_new_name.png" alt="bgppeering_obj_new_name"></p>
<p>Much better, now the BGP Peering object names mean something. We can see the local device the peering is on and the AS number of remote peer.</p>
<p><a name="enforce-the-same"></a></p>
<h2 id="enforcethesamenetworkforlocalandremoteips">Enforce the same network for local and remote IPs</h2>
<p>Local IP address is selected from the list of IP addresses available in NetBox when creating new BGP Peering. But when we add remote IP address we have to type it in manually. If you try entering remote IP that is in a network different than the one local IP is in our form will happily accept it.</p>
<p>It perhaps is not that much of an issue but I thought this is a good place to show you how we can add custom validation to forms.</p>
<p><a name="add-validation-t"></a></p>
<h3 id="addvalidationtoformclass">Add validation to form class</h3>
<p>We'll update class <code>BgpPeeringForm</code> in <code>forms.py</code> with code that performs the required validation.</p>
<p>To validate multiple form fields we can use method <code>clean()</code><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> which comes from Django <code>Form</code> class.</p>
<p>Let's implement our check:</p>
<pre><code class="language-python"># forms.py

import ipaddress

class BgpPeeringForm(BootstrapMixin, forms.ModelForm):
...
    def clean(self):
        cleaned_data = super().clean()
        local_ip = cleaned_data.get(&quot;local_ip&quot;)
        remote_ip = cleaned_data.get(&quot;remote_ip&quot;)

        if local_ip and remote_ip:
            # We can trust these are valid IP addresses. Address format validation done in .super()
            if (
                ipaddress.ip_interface(str(local_ip)).network
                != ipaddress.ip_interface(str(remote_ip)).network
            ):
                msg = &quot;Local IP and Remote IP must be in the same network.&quot;
                self.add_error(&quot;local_ip&quot;, msg)
                self.add_error(&quot;remote_ip&quot;, msg)
</code></pre>
<p>First, we import <code>ipaddress</code> library which we'll use to check if IPs belong to the same network.</p>
<p>Next, we go inside of <code>clean()</code> method.</p>
<p>To trigger field validation of individual fields and retrieve the results we call <code>clean()</code> method from the parent class.</p>
<pre><code class="language-python">cleaned_data = super().clean()
</code></pre>
<p>Now that we have validated field values in <code>cleaned_data</code> we can retrieve strings coming from local IP and remote IP form fields.</p>
<pre><code>local_ip = cleaned_data.get(&quot;local_ip&quot;)
remote_ip = cleaned_data.get(&quot;remote_ip&quot;)
</code></pre>
<p>With values retrieved we write conditional to check if any of them are empty. This should never happen but we might want more processing here in the future so it's prudent to do so.</p>
<pre><code>if local_ip and remote_ip:
</code></pre>
<p>Next, we have conditional implementing network checking logic. We use <code>ip_interface</code> <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> function from <code>ipaddress</code> module to create <code>IPv4Interface</code> or <code>IPv4Interface</code> objects from each of our IP addresses.</p>
<p>The interface objects have a handy <code>network</code> attribute that returns IP network the interface belongs to. This allows us to directly compare networks for peering IPs.</p>
<p>If networks are the same we take no action, i.e. form returns with no errors. However, if the networks differ we generate an error message and assign it to <code>msg</code> variable. We then append this error to validation results for <code>local_ip</code> and <code>remote_ip</code> form fields.</p>
<pre><code>if (
    ipaddress.ip_interface(str(local_ip)).network
    != ipaddress.ip_interface(str(remote_ip)).network
):
    msg = &quot;Local IP and Remote IP must be in the same network.&quot;
    self.add_error(&quot;local_ip&quot;, msg)
    self.add_error(&quot;remote_ip&quot;, msg)
</code></pre>
<p><a name="show-errors-on-t"></a></p>
<h3 id="showerrorsontheforminwebui">Show errors on the form in web UI</h3>
<p>We now have code handling validation and generating errors. We now have code handling validation and generating errors. The next step is to update the form template so that the errors show up in the web UI.</p>
<p>I added <code>{% if field.errors %}</code> block to div rendering fields in <code>bgppeering_edit.html</code>:</p>
<pre><code>&lt;div class=&quot;col-md-9&quot;&gt;
    {{ field }}
    {% if field.help_text %}
    &lt;span class=&quot;help-block&quot;&gt;{{ field.help_text|safe }}&lt;/span&gt;
    {% endif %}
    {% if field.errors %}
    &lt;ul&gt;
        {% for error in field.errors %}
        &lt;li class=&quot;text-danger&quot;&gt;{{ error }}&lt;/li&gt;
        {% endfor %}
    &lt;/ul&gt;
    {% endif %}
&lt;/div&gt;
</code></pre>
<p>When form fails validation, errors for each form field will be returned in <code>field.errors</code> attribute [^field_erros]. We can then loop over errors and display them under a given field.</p>
<p>Below is an example where I entered IP addresses belonging to different networks. You can see that the form failed validation and errors are displayed under each of the fields.</p>
<p><img src="https://ttl255.com/content/images/2021/02/ip_fields_errors.png" alt="ip_fields_errors"></p>
<p><a name="allowing-objects"></a></p>
<h2 id="allowingobjectstobedeleted">Allowing objects to be deleted</h2>
<p>We have been happily adding BGP Peerings objects but at some point, we might have to delete some of them. Unfortunately, the only way to do it right now is from the admin panel. This is less than ideal and thus we decide to implement delete functionality.</p>
<p><a name="creating-delete"></a></p>
<h3 id="creatingdeleteview">Creating delete view</h3>
<p>There are few places we need to modify, let's start with <code>views.py</code>.</p>
<pre><code>from django.urls import reverse_lazy
...
from django.views.generic.edit import CreateView, DeleteView

...

class BgpPeeringDeleteView(DeleteView):
    &quot;&quot;&quot;View for deleting a BgpPeering instance&quot;&quot;&quot;

    model = BgpPeering
    success_url = reverse_lazy(&quot;plugins:netbox_bgppeering:bgppeering_list&quot;)
    template_name = &quot;netbox_bgppeering/bgppeering_delete.html&quot;
</code></pre>
<p>We import <code>reverse_lazy</code> function which will be used to return the page for the given namespace URL.</p>
<p>We also borrow another of Django's generic edit views, <code>DeleteView</code><sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>, which will offload a lot of required work. This class class will handle both <code>GET</code> and <code>POST</code> methods.</p>
<ul>
<li>If <code>GET</code> is used view will return the confirmation page. That page should contain a form that will <code>POST</code> to the URL pointing to this view.</li>
<li>If <code>POST</code> is used then the provided object will be deleted.</li>
</ul>
<p>For our plugin, we will go down the route of <code>GET</code> followed by <code>POST</code> from the confirmation page form.</p>
<p>The actual view is relatively short:</p>
<ul>
<li>We define <code>BgpPeeringDeleteView</code> class inheriting from <code>DeleteView</code>.</li>
<li><code>model</code> variable takes model class for this view, here <code>BgpPeering</code>.</li>
<li><code>success_url</code> defines URL to which view will redirect after an object has been deleted. Here I provide a namespaced URL pointing to list view of our records.</li>
<li><code>template_name</code> points to the template that will be rendered when we're asked to confirm the deletion.</li>
</ul>
<p>Next, we look at the deletion confirmation template.</p>
<p><a name="delete-confirmat"></a></p>
<h3 id="deleteconfirmationtemplate">Delete confirmation template</h3>
<p>Deleting objects is a serious business so I thought that we should customize the deletion confirmation page. It will also make it look like other prompts in NetBox.</p>
<p>Here is our template:</p>
<pre><code># bgppeering_delete.html 
{% extends 'base.html' %}
{% load form_helpers %}

{% block content %}
&lt;div class=&quot;row&quot;&gt;
	&lt;div class=&quot;col-md-6 col-md-offset-3&quot;&gt;
        &lt;form action=&quot;&quot; method=&quot;post&quot; class=&quot;form&quot;&gt;
            {% csrf_token %}
            &lt;div class=&quot;panel panel-{{ panel_class|default:&quot;danger&quot; }}&quot;&gt;
                &lt;div class=&quot;panel-heading&quot;&gt;Delete BGP Peering?&lt;/div&gt;
                &lt;div class=&quot;panel-body&quot;&gt;
                    &lt;p&gt;Are you sure you want to delete BGP Peering &lt;strong&gt;{{ object }}&lt;/strong&gt;?&lt;/p&gt;
                    &lt;div class=&quot;text-right&quot;&gt;
                        &lt;button type=&quot;submit&quot; name=&quot;_confirm&quot; class=&quot;btn btn-{{ button_class|default:&quot;danger&quot; }}&quot;&gt;Confirm&lt;/button&gt;
                        &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering' pk=bgppeering.pk %}&quot; class=&quot;btn btn-default&quot;&gt;Cancel&lt;/a&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/form&gt;
	&lt;/div&gt;
&lt;/div&gt;
{% endblock %} 
</code></pre>
<p>This is a pretty standard form. We have some styling to make users aware of the seriousness of the operation.</p>
<p>Of interest are two buttons, <code>Confirm</code> and <code>Cancel</code>.</p>
<ul>
<li><code>Confirm</code> will submit the form to the URL pointing to our delete view. This will result in the <code>POST</code> action triggering deletion of the object.</li>
<li><code>Cancel</code> will take us back to the detailed view of the object instead of deleting it.</li>
</ul>
<p><a name="add-url-to-delet"></a></p>
<h3 id="addurltodeleteview">Add url to delete view</h3>
<p>We created a delete view but there's no way to reach it currently. We need to add URL pointing to it.</p>
<p>Let's add the missing URL:</p>
<pre><code># urls.py
from .views import (
    BgpPeeringCreateView,
    BgpPeeringDeleteView,
    BgpPeeringListView,
    BgpPeeringView,
)


urlpatterns = [
...
    path(&quot;&lt;int:pk&gt;/delete/&quot;, BgpPeeringDeleteView.as_view(), name=&quot;bgppeering_delete&quot;),
]
</code></pre>
<p>We added <code>BgpPeeringDeleteView</code> class to the import list. Then we created new entry in <code>urlpatterns</code>:</p>
<pre><code>path(&quot;&lt;int:pk&gt;/delete/&quot;, BgpPeeringDeleteView.as_view(), name=&quot;bgppeering_delete&quot;)
</code></pre>
<p>This is like URL for a detailed view. We use <code>pk</code> attribute again, which is what<code>BgpPeeringDeleteView</code> will need to locate the object for deletion. We name this URL <code>bgppeering_delete</code> to make referring to it easier.</p>
<p><a name="adding-object-vi"></a></p>
<h3 id="addingobjectviewheaderanddeletebutton">Adding object view header and delete button</h3>
<p>The last thing we're missing is some kind of button in UI that would allow us to delete objects. This is what we're going to do next.</p>
<p>I'm going to edit the detailed object view template and add a few things:</p>
<ul>
<li>Breadcrumb with link to the list of objects followed by name of the current object.</li>
<li>Date showing when this object was created.</li>
<li>Time elapsed since this object was last updated.</li>
<li>Delete object button leading to delete confirmation page.</li>
</ul>
<pre><code># bgppeering.html
{% block header %}
&lt;div class=&quot;row noprint&quot;&gt;
    &lt;div class=&quot;col-sm-8&quot;&gt;
        &lt;ol class=&quot;breadcrumb&quot;&gt;
            &lt;li&gt;&lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_list' %}&quot;&gt;BGP Peerings&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;{{ bgppeering }}&lt;/li&gt;
        &lt;/ol&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;col-sm-8&quot;&gt;
    &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_delete' pk=bgppeering.pk %}&quot; class=&quot;btn btn-danger pull-right&quot;&gt;
        &lt;span class=&quot;{{ icon_classes.trash }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Delete
    &lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;col-sm-8&quot;&gt;
    &lt;h1&gt;{% block title %}{{ bgppeering }}{% endblock %}&lt;/h1&gt;
    &lt;p&gt;
        &lt;small class=&quot;text-muted&quot;&gt;Created {{ bgppeering.created }} &amp;middot; Updated &lt;span
                title=&quot;{{ bgppeering.last_updated }}&quot;&gt;{{ bgppeering.last_updated|timesince }}&lt;/span&gt; ago&lt;/small&gt;
    &lt;/p&gt;
&lt;/div&gt;

{% endblock %}
</code></pre>
<p>As you can see I'm overriding block <code>header</code> which comes from <code>base.html</code>. This block is used for elements displayed below the main menu but above the rest of the content.</p>
<p>Then we have 3 divs:</p>
<ul>
<li>Div with breadcrumb where we have URL for the page with list objects, followed by the name of the current object.</li>
<li>Div with button pointing to URL responsible for deleting the object. We pass this URL the <code>pk</code> value identifying the current object.</li>
<li>Div containing the name of the object, as a title. This is followed by the object creation date and time since this object was last updated.</li>
</ul>
<p>If you were to run the code we added up to this point you should see the object details page looking like the below one:</p>
<p><img src="https://ttl255.com/content/images/2021/02/obj_detail_del.png" alt="obj_detail_del"></p>
<p>And when we click <code>Delete</code> button we should be presented with the confirmation prompt:</p>
<p><img src="https://ttl255.com/content/images/2021/02/obj_del_confirm.png" alt="obj_del_confirm"></p>
<p>That's pretty cool, we can now add, and delete objects. This thing is starting to look better and better.</p>
<p>But we're not done here. Why not add edit functionality so that we can modify existing objects?</p>
<p><a name="allowing-objects"></a></p>
<h2 id="allowingobjectstobeedited">Allowing objects to be edited</h2>
<p>You might be happy to know that to enable editing we can reuse most of the template code used for object creation. We will need to modify it and create an edit view but it takes less work than adding object deletion.</p>
<p><a name="creating-edit-vi"></a></p>
<h3 id="creatingeditview">Creating edit view</h3>
<p>To create the edit view class I added the below code to <code>views.py</code>:</p>
<pre><code># views.py
from django.views.generic.edit import CreateView, DeleteView, UpdateView
...
class BgpPeeringEditView(UpdateView):
    &quot;&quot;&quot;View for editing a BgpPeering instance.&quot;&quot;&quot;

    model = BgpPeering
    form_class = BgpPeeringForm
    template_name = &quot;netbox_bgppeering/bgppeering_edit.html&quot;
</code></pre>
<p>We complete our collection of generic views by importing <code>UpdateView</code> <sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>. As usual, this will do a lot of hard work for us.</p>
<p>Then comes our class, <code>BgpPeeringEditView</code>. In this class we define:</p>
<ul>
<li><code>model</code> - Model class used for this class, here <code>BgpPeering</code>.</li>
<li><code>form_class</code> - Form used by the view and template. We reuse the form we built for the object creation view, <code>BgpPeeringForm</code>.</li>
<li><code>template_name</code> - Template used by this view, again, we reuse existing template <code>bgppeering_edit.html</code>.</li>
</ul>
<p>That was nice, we reused existing components and plugged them into edit view.</p>
<p>Next, we need to modify the template so it can support both creating and editing objects.</p>
<p><a name="modify-object-cr"></a></p>
<h3 id="modifyobjectcreationtemplate">Modify object creation template</h3>
<p>As I mentioned, we can mostly reuse the existing template, <code>bgppeering_edit.html</code>, with some mall modifications.</p>
<pre><code># bgppeering_edit.html
...
    {% block title %}
    {% if object.pk %}
        Editing BGP Peering - {{ object }}
    {% else %}
        Add a new BGP Peering
    {% endif %}
    {% endblock %}
...
    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-md-6 col-md-offset-3 text-right&quot;&gt;
            {% block buttons %}
            &lt;button type=&quot;submit&quot; name=&quot;_create&quot; class=&quot;btn btn-primary&quot;&gt;Create&lt;/button&gt;
            {% endblock %}
            {% if object.pk %}
                &lt;button type=&quot;submit&quot; name=&quot;_update&quot; class=&quot;btn btn-primary&quot;&gt;Update&lt;/button&gt;
                &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering' pk=bgppeering.pk %}&quot; class=&quot;btn btn-default&quot;&gt;Cancel&lt;/a&gt;
            {% else %}
                &lt;button type=&quot;submit&quot; name=&quot;_create&quot; class=&quot;btn btn-primary&quot;&gt;Create&lt;/button&gt;
                &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_list' %}&quot; class=&quot;btn btn-default&quot;&gt;Cancel&lt;/a&gt;
            {% endif %}
        &lt;/div&gt;
</code></pre>
<p>The first thing we're doing here is checking if <code>object.pk</code> has value. Edit view will automatically pass <code>pk</code> to the template. So if <code>pk</code> has value then we're dealing with object edit and display appropriate title. Otherwise, we display the original title for adding a new object.</p>
<p>Next, we modify the section showing buttons below the form. Here we also add conditional, checking if <code>object.pk</code> has value.</p>
<ul>
<li>If conditional evaluates to <code>True</code> it means we're dealing with object edit action. We present two buttons, <code>Update</code> will save changes made to the object. <code>Cancel</code> will take us back to the object details view.</li>
<li>If conditional evaluates to <code>False</code> we follow the logic we built originally, an object is either created or we're sent to the list view.</li>
</ul>
<p>And that's it, our template now supports both object creation and editing.</p>
<p><a name="adding-editing-b"></a></p>
<h3 id="addingeditingbuttonandurl">Adding editing button and URL</h3>
<p>Most of the work is done here. We should now add URL pointing to the edit view and edit button that will allow users to edit the object.</p>
<pre><code># urls.py
from .views import (
    BgpPeeringCreateView,
    BgpPeeringDeleteView,
    BgpPeeringEditView,
    BgpPeeringListView,
    BgpPeeringView,
)

urlpatterns = [
...
    path(&quot;&lt;int:pk&gt;/edit/&quot;, BgpPeeringEditView.as_view(), name=&quot;bgppeering_edit&quot;),
]
</code></pre>
<p>This URL is similar to the one for deleting an object. We again have <code>pk</code> argument which we pass to class view. We use a friendly name for URL, <code>bgppeering_edit</code>.</p>
<p>Finally we will add edit button next to the delete button on object details view page:</p>
<pre><code># bgppeering_edit.html
    &lt;div class=&quot;pull-right noprint&quot;&gt;
        &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_edit' pk=bgppeering.pk %}&quot; class=&quot;btn btn-warning&quot;&gt;
            &lt;span class=&quot;{{ icon_classes.pencil }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Edit
        &lt;/a&gt;
        &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_delete' pk=bgppeering.pk %}&quot; class=&quot;btn btn-danger&quot;&gt;
            &lt;span class=&quot;{{ icon_classes.trash }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Delete
        &lt;/a&gt;
    &lt;/div&gt;
</code></pre>
<p>And that's it, we should now be able to edit given object. Let's give it a try.</p>
<p>Here's the page with object details, notice <code>Edit</code> button.</p>
<p><img src="https://ttl255.com/content/images/2021/02/object_view_edit_button.png" alt="object_view_edit_button"></p>
<p>And here's the edit view that we'll get after the edit button is clicked.</p>
<p><img src="https://ttl255.com/content/images/2021/02/object_edit_form.png" alt="object_edit_form"></p>
<p>Another useful addition to our plugin is complete.</p>
<p><a name="prettifying-fiel"></a></p>
<h2 id="prettifyingfieldlabels">Prettifying field labels</h2>
<p>Current field labels displayed in the list view and few other places are derived from model fields. Not all of them look pretty so we should work on making them look better.</p>
<p><img src="https://ttl255.com/content/images/2021/02/object_field_names_old.png" alt="object_field_names_old"></p>
<p>Let's override default field names in model class in <code>models.py</code>.</p>
<pre><code># models.py
class BgpPeering(ChangeLoggedModel):
...
    local_ip = models.ForeignKey(
        to=&quot;ipam.IPAddress&quot;, on_delete=models.PROTECT, verbose_name=&quot;Local IP&quot;
    )
    local_as = ASNField(help_text=&quot;32-bit ASN used locally&quot;, verbose_name=&quot;Local ASN&quot;)
    remote_ip = IPAddressField(
        help_text=&quot;IPv4 or IPv6 address (with mask)&quot;, verbose_name=&quot;Remote IP&quot;
    )
    remote_as = ASNField(help_text=&quot;32-bit ASN used by peer&quot;, verbose_name=&quot;Remote ASN&quot;)
    peer_name = models.CharField(max_length=64, blank=True, verbose_name=&quot;Peer Name&quot;)
</code></pre>
<p>To customize the text displayed for the given field we pass the argument <code>verbose_name</code> <sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup> when creating field objects. The value of each of the arguments will be the text we want to display as the field name.</p>
<p>We will also do the same for the search filter. In this case, we edit form class in <code>forms.py</code>.</p>
<pre><code># forms.py
class BgpPeeringFilterForm(BootstrapMixin, forms.ModelForm):
...
    local_as = forms.IntegerField(
        required=False,
        label=&quot;Local ASN&quot;,
    )

    remote_as = forms.IntegerField(
        required=False,
    )
    remote_as = forms.IntegerField(required=False, label=&quot;Remote ASN&quot;)

    peer_name = forms.CharField(
        required=False,
        label=&quot;Peer Name&quot;,
    )
</code></pre>
<p>We add <code>label</code> <sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup> argument to each form field we want to customize.</p>
<p>That's it, let's see how the fields are looking like now.</p>
<p><img src="https://ttl255.com/content/images/2021/02/object_field_names_new.png" alt="object_field_names_new"></p>
<p>Perfect, names and labels are looking better now.</p>
<p>We'll finish our small improvements by adding <code>Add</code> button to the list view in <code>bgppeering_list.html</code>.</p>
<pre><code># bgppeering_list.html 
...
{% block content %}
&lt;div class=&quot;pull-right noprint&quot;&gt;
    &lt;a href=&quot;{% url 'plugins:netbox_bgppeering:bgppeering_add' %}&quot; class=&quot;btn btn-primary&quot;&gt;
        &lt;span class=&quot;{{ icon_classes.plus }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Add
    &lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>And with this last modification we've come to an end of this post.</p>
<p>Source code with all the modifications we made up to this point is in branch <code>small-improvements</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/small-improvements" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/small-improvements</a></p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>In the previous posts of this series, we were focusing on getting the plugin off the ground. We implemented models, and several views and forms. Here we had a closer look and added improvements that make the plugin easier to use. It might not seem like much but these kinds of small additions can make our lives much easier.</p>
<p>The next post will be the last in the series. We'll complete our plugin by adding user permissions as well as API calls. See you next time!</p>
<p><a name="resources"></a></p>
<h2 id="resources">Resources</h2>
<ul>
<li>NetBox docs: Plugin Development: <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></li>
<li>NetBox source code on GitHub: <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a></li>
<li>NetBox Extensibility Overview, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=FSoCzuWOAE0" target="_blank">https://www.youtube.com/watch?v=FSoCzuWOAE0</a></li>
<li>NetBox Plugins Development, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=LUCUBPrTtJ4" target="_blank">https://www.youtube.com/watch?v=LUCUBPrTtJ4</a></li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="https://docs.djangoproject.com/en/3.1/ref/forms/api/#django.forms.Form.clean" target="_blank">https://docs.djangoproject.com/en/3.1/ref/forms/api/#django.forms.Form.clean</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p><a href="https://docs.python.org/3/library/ipaddress.html#ipaddress.ip_interface" target="_blank">https://docs.python.org/3/library/ipaddress.html#ipaddress.ip_interface</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p><a href="https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView" target="_blank">https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p><a href="https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#django.views.generic.edit.UpdateView" target="_blank">https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#django.views.generic.edit.UpdateView</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p><a href="https://docs.djangoproject.com/en/3.1/topics/db/models/#verbose-field-names" target="_blank">https://docs.djangoproject.com/en/3.1/topics/db/models/#verbose-field-names</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p><a href="https://docs.djangoproject.com/en/3.1/ref/forms/fields/#label" target="_blank">https://docs.djangoproject.com/en/3.1/ref/forms/fields/#label</a> <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Developing NetBox Plugin - Part 3 - Adding search panel]]></title><description><![CDATA[Learn how to build your own NetBox plugins. In this post we add search functionality to our BgpPeering plugin.]]></description><link>https://ttl255.com/developing-netbox-plugin-part-3-adding-search/</link><guid isPermaLink="false">5ffed0eab430486ddc56ece0</guid><category><![CDATA[netbox]]></category><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[tools]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Wed, 13 Jan 2021 11:41:23 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Welcome to part 3 of my tutorial walking you through process of developing NetBox plugin. In part 2 we added basic web UI views to our BgpPeering plugin. In this post we'll add search panel to list view to allow us to search/filter Bgp Peering objects.</p>
<h2 id="developingnetboxplugintutorialseries">Developing NetBox Plugin tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/">Developing NetBox Plugin - Part 1 - Setup and initial build</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/">Developing NetBox Plugin - Part 2 - Adding web UI pages</a></li>
<li>Developing NetBox Plugin - Part 3 - Adding search panel</li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/">Developing NetBox Plugin - Part 4 - Small improvements</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/">Developing NetBox Plugin - Part 5 - Permissions and API</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#filter-class">Filter class</a></li>
<li><a href="#search-form-clas">Search form class</a></li>
<li><a href="#adding-form-to-l">Adding form to list view template</a>
<ul>
<li><a href="#icon-classes">Icon classes</a></li>
</ul>
</li>
<li><a href="#adding-filtering">Adding filtering to list view class</a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#resources">Resources</a></li>
<li><a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-list-search" target="_blank">BGP Peering Plugin GitHub repository</a></li>
</ul>
<p><a name="introduction"></a></p>
<h2 id="introduction">Introduction</h2>
<p>List view we created for displaying all Bgp Peering objects in one place is very useful. However it will become difficult to find items of interest once we have more than 30-50 objects. For that purpose we should add means of filtering objects to the ones that meet certain criteria.</p>
<p>Other objects in NetBox already have filtering functionality and use search panel located to the right of object tables. We'll try and replicate the NetBox look and feel when upgrading our view.</p>
<p>In our case, I want to be able to filter Bgp Peering objects by:</p>
<ul>
<li><code>site</code> - This should be a drop-down list with NetBox defined sites.</li>
<li><code>device</code> - Again, drop-down list but with NetBox defined devices.</li>
<li><code>local_as</code> - BGP ASN used locally, it has to be an exact match.</li>
<li><code>remote_as</code> - BGP ASN used by 3rd party, it has to be an exact match.</li>
<li><code>peer_name</code> - Name of the peer, we want to allow partial matches.</li>
<li><code>q</code> - Generic query field that looks at peer name and descriptions. We want to allow partial matches here.</li>
</ul>
<p>To make all this work we need a few components:</p>
<ul>
<li>filter class</li>
<li>search form class</li>
<li>updated list view template</li>
<li>updated list view class</li>
</ul>
<p><a name="filter-class"></a></p>
<h2 id="filterclass">Filter class</h2>
<p>First component we're going to work on is filter class. To help us with this we're using Django app called <code>django_filters</code><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. This app makes it easier to build model based filtering that will serve as an abstraction used by our list view.</p>
<p>Filtering class is going to be recorded in <code>filters.py</code> file.</p>
<pre><code class="language-python"># filters.py
import django_filters
from django.db.models import Q

from dcim.models import Device, Site

from .models import BgpPeering


class BgpPeeringFilter(django_filters.FilterSet):
    &quot;&quot;&quot;Filter capabilities for BgpPeering instances.&quot;&quot;&quot;

    q = django_filters.CharFilter(
        method=&quot;search&quot;,
        label=&quot;Search&quot;,
    )

    site = django_filters.ModelMultipleChoiceFilter(
        field_name=&quot;site__slug&quot;,
        queryset=Site.objects.all(),
        to_field_name=&quot;slug&quot;,
    )

    device = django_filters.ModelMultipleChoiceFilter(
        field_name=&quot;device__name&quot;,
        queryset=Device.objects.all(),
        to_field_name=&quot;name&quot;,
    )

    peer_name = django_filters.CharFilter(
        lookup_expr=&quot;icontains&quot;,
    )

    class Meta:
        model = BgpPeering

        fields = [
            &quot;local_as&quot;,
            &quot;remote_as&quot;,
            &quot;peer_name&quot;,
        ]

    def search(self, queryset, name, value):
        &quot;&quot;&quot;Perform the filtered search.&quot;&quot;&quot;
        if not value.strip():
            return queryset
        qs_filter = Q(peer_name__icontains=value) | Q(description__icontains=value)
        return queryset.filter(qs_filter)

</code></pre>
<p>Since all this is new, I'm going to go through the code in detail.</p>
<p>We start building filter by creating <code>BgpPeeringFilter</code> class that inherits from <code>django_filters.FilterSet</code>.</p>
<p>Next, in <code>class Meta</code>, we define model this filter is based on.</p>
<p>In <code>fields</code> we specify list of fields for which filters will be auto-generated. This is based on the model definitions. Any fields that don't need special treatment can go in here.</p>
<pre><code>class Meta:
    model = BgpPeering

    fields = [
        &quot;local_as&quot;,
        &quot;remote_as&quot;,
        &quot;peer_name&quot;,
    ]
</code></pre>
<p>Fields that need customization, like linking to other models, or partial matching, we will define as class attributes.</p>
<p>Let's have a look at those fields now.</p>
<ul>
<li>
<p><code>site</code> - We want this to be a drop down-menu with multiple choices. To do that we make this an instance of <code>django_filters.ModelMultipleChoiceFilter</code> class.</p>
<pre><code>site = django_filters.ModelMultipleChoiceFilter
</code></pre>
<p>We initialize the object with three arguments <code>field_name</code>, <code>queryset</code> and <code>to_field_name</code>.</p>
<ul>
<li>
<p><code>field_name</code> - Specifies attribute on the model field that we will filter <code>BgpPeering</code> objects on. Here we use <code>site</code> field from our model and its attribute <code>slug</code>. Attribute follows <code>__</code> (double underscore) after name of the field:</p>
<pre><code>field_name=&quot;site__slug&quot;
</code></pre>
</li>
<li>
<p><code>queryset</code> - This defines collection of <code>Site</code> objects filter will present as filtering choices. We imported <code>Site</code> model from <code>dcim.models</code> and return all of the available objects.</p>
</li>
<li>
<p><code>to_field_name</code> - Here we specify name of the attribute, <code>slug</code>, that filter will take from <code>Site</code> object and apply to <code>field_name</code> we specified earlier.</p>
</li>
</ul>
</li>
<li>
<p><code>device</code> - Similar to <code>site</code>, it's a drop-down menu with multiple choices. Here we link to <code>Device</code> NetBox model from which filter will take <code>name</code> attribute. We filter BgpPeering objects using field/attribute combination of <code>device__name</code>.</p>
</li>
<li>
<p><code>peer_name</code> - Peer name is a character field, so we use <code>django_filters.CharFilter</code> class to define it.<br>
We want to allow case insensitive partial matches here. To do that we pass argument <code>lookup_expr=&quot;icontains&quot;</code> when creating object.<br>
Default lookup method is <code>exact</code> which forces exact matches. See docs for available lookup methods <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
</li>
<li>
<p><code>q</code> - This is general, string, query field. We want it to be of character type <code>django_filters.CharFilter</code>.<br>
In <code>method</code> argument we specify Python method, here <code>search</code>, that should be called when this field is used <sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.<br>
And <code>label</code> field is the text that will appear in the field as a hint.</p>
<pre><code>q = django_filters.CharFilter(
    method=&quot;search&quot;,
    label=&quot;Search&quot;,
)
</code></pre>
<p>Next we define method <code>search</code>, inside of the class, which <code>q</code> field filter will use. You can define these methods outside of the class scope but in most cases it makes sense to keep it close to the field definition.</p>
<pre><code>def search(self, queryset, name, value):
    &quot;&quot;&quot;Perform the filtered search.&quot;&quot;&quot;
    if not value.strip():
        return queryset
    qs_filter = Q(peer_name__icontains=value) | Q(description__icontains=value)
    return queryset.filter(qs_filter)  
</code></pre>
<p>Methods we define for filter fields will be passed <code>queryset</code>, <code>name</code> and <code>value</code> arguments. Hence why our method has <code>(self, queryset, name, value)</code> signature.</p>
<ul>
<li><code>queryset</code> - This is essentially list of objects currently meeting filter criteria, before <code>q</code> filter is applied.</li>
<li><code>name</code> - Name of the filter field, here <code>q</code>.</li>
<li><code>value</code> - Value entered into the filter field in web GUI.</li>
</ul>
<p>Logic in our method is relatively simple. We return unchanged queryset if no value was provided:</p>
<pre><code>if not value.strip():
        return queryset
</code></pre>
<p>If there is a value we take advantage of Django <code>Q</code><sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup> object to build a query based on two fields.</p>
<ul>
<li><code>Q(peer_name__icontains=value)</code> - We check, ignoring case, if <code>peer_name</code> field contains <code>value</code>.</li>
<li><code>Q(description__icontains=value)</code> - We check, ignoring case, if <code>description</code> field contains <code>value</code>.</li>
</ul>
<p>Then we combine these objects with <code>|</code> - logical OR - operator and assign the result to <code>qs_filter</code> variable.</p>
<p><code>qs_filter = Q(peer_name__icontains=value) | Q(description__icontains=value)</code></p>
<p>Finally, we apply this filter to queryset and return the result.</p>
<p><code>return queryset.filter(qs_filter)</code></p>
<p>End effect is that if <code>value</code> is contained in either <code>peer_name</code> or peering <code>description</code> for given object then the object will be included in final queryset.</p>
</li>
</ul>
<p>With that we finish filter class and move on to the form.</p>
<p><a name="search-form-clas"></a></p>
<h2 id="searchformclass">Search form class</h2>
<p>Next step in our quest for filtering is creating class that will represent the search form.</p>
<p>We will add the below to <code>forms.py</code>:</p>
<pre><code># forms.py
from dcim.models import Device, Site
 
class BgpPeeringFilterForm(BootstrapMixin, forms.ModelForm):
    &quot;&quot;&quot;Form for filtering BgpPeering instances.&quot;&quot;&quot;

    q = forms.CharField(required=False, label=&quot;Search&quot;)

    site = forms.ModelChoiceField(
        queryset=Site.objects.all(), required=False, to_field_name=&quot;slug&quot;
    )

    device = forms.ModelChoiceField(
        queryset=Device.objects.all(),
        to_field_name=&quot;name&quot;,
        required=False,
    )

    local_as = forms.IntegerField(
        required=False,
    )

    remote_as = forms.IntegerField(
        required=False,
    )

    peer_name = forms.CharField(
        required=False,
    )

    class Meta:
        model = BgpPeering
        fields = []
</code></pre>
<p>Let's break this code down.</p>
<p>We create <code>BgpPeeringFilterForm</code> that inherits from <code>BootstrapMixin</code> and <code>forms.ModelForm</code>. This is just like the form we defined in part 2 of this tutorial.</p>
<p>Next we define model we're building this form from, <code>BgpPeering</code>, and fields that will be auto-generated. I want to define all fields explicitly so <code>fields</code> attribute will be an empty list. You need to include this attribute even if you don't list anything here or an exception will be raised.</p>
<pre><code>class Meta:
    model = BgpPeering
    fields = []
</code></pre>
<p>Following that we will define types and attributes of the fields used by search form.</p>
<ul>
<li>
<p><code>q</code> - General query field should be a character field so we use type <code>forms.CharField</code>. This field is optional and we manually set label of this field to <code>Search</code>.</p>
</li>
<li>
<p><code>site</code> - Site selection field, this field is optional.</p>
<ul>
<li>To link it to NetBox <code>Site</code> objects we make it of type <code>forms.ModelChoiceField</code>.</li>
<li>We ask for all <code>Site</code> objects to be available in drop-down with <code>queryset=Site.objects.all()</code>.</li>
<li>When given item is selected we want <code>slug</code> attribute to be returned. This is what <code>to_field_name=&quot;slug&quot;</code> does.</li>
</ul>
</li>
<li>
<p><code>device</code> - Similar to <code>site</code> but here we link to <code>Device</code> model and ask for <code>name</code> attribute to be returned.</p>
</li>
<li>
<p><code>local_as</code> and <code>remote_as</code> - Optional integer fields, we use type <code>forms.IntegerField</code> for those.</p>
</li>
<li>
<p><code>peer_name</code> - Simple, optional, character field. We use type <code>forms.CharField</code> for it.</p>
</li>
</ul>
<p>And that's our form class done.</p>
<p><a name="adding-form-to-l"></a></p>
<h2 id="addingformtolistviewtemplate">Adding form to list view template</h2>
<p>To allow users to search through BgpPeering objects we need to update our web UI page. We'll add search panel on the right side of the object list view template.</p>
<p>We add new <code>&lt;div&gt;</code> in <code>bgppeering_list.html</code> right after the one we created in part 2:</p>
<pre><code class="language-html">&lt;div class=&quot;col-md-9&quot;&gt;
    {% render_table table %}
&lt;/div&gt;
&lt;!-- search panel div start --&gt;
&lt;div class=&quot;col-md-3 noprint&quot;&gt;
    &lt;div class=&quot;panel panel-default&quot;&gt;
        &lt;div class=&quot;panel-heading&quot;&gt;
            &lt;span class=&quot;{{ icon_classes.search }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;
            &lt;strong&gt;Search&lt;/strong&gt;
        &lt;/div&gt;
        &lt;div class=&quot;panel-body&quot;&gt;
            &lt;form action=&quot;.&quot; method=&quot;get&quot; class=&quot;form&quot;&gt;
                {% for field in filter_form.visible_fields %}
                &lt;div class=&quot;form-group&quot;&gt;
                    {% if field.name == &quot;q&quot; %}
                    &lt;div class=&quot;input-group&quot;&gt;
                        &lt;input type=&quot;text&quot; name=&quot;q&quot; class=&quot;form-control&quot; placeholder=&quot;{{ field.label }}&quot;
                            {% if request.GET.q %}value=&quot;{{ request.GET.q }}&quot; {% endif %} /&gt;
                        &lt;span class=&quot;input-group-btn&quot;&gt;
                            &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;
                                &lt;span class=&quot;{{ icon_classes.search }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;
                            &lt;/button&gt;
                        &lt;/span&gt;
                    &lt;/div&gt;
                    {% else %}
                    {{ field.label_tag }}
                    {{ field }}
                    {% endif %}
                &lt;/div&gt;
                {% endfor %}
                &lt;div class=&quot;text-right noprint&quot;&gt;
                    &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;
                        &lt;span class=&quot;{{ icon_classes.search }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Apply
                    &lt;/button&gt;
                    &lt;a href=&quot;.&quot; class=&quot;btn btn-default&quot;&gt;
                        &lt;span class=&quot;{{ icon_classes.remove }}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; Clear
                    &lt;/a&gt;
                &lt;/div&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>We make this div 3 columns wide. Inside we place panel header and search form divs.</p>
<p>Form fields are rendered in <code>for</code> loop:</p>
<pre><code>{% for field in filter_form.visible_fields %}
...
{% endfor %}
</code></pre>
<p>These fields are taken from form class we defined earlier. We leave all fields, with exception of <code>q</code> field, to their defaults by displaying field label followed by rendering actual field.</p>
<pre><code>{{ field.label_tag }}
{{ field }}
</code></pre>
<p>Because <code>q</code> field does not belong to underlying model we handle it differently. We make it a text input field with label as a placeholder.</p>
<p>The interesting bit here is that the value of the field is carried over from previous search. This is done for us for auto-generated form fields but here we have to do it manually with the below expression:</p>
<p><code>{% if request.GET.q %}value=&quot;{{ request.GET.q }}&quot; {% endif %} /&gt;</code></p>
<p>Remaining html/css code is for layout and visual elements.</p>
<p><a name="icon-classes"></a></p>
<h3 id="iconclasses">Icon classes</h3>
<p>You might have noticed a few references to <code>icon_classes</code> variable here like in <code>class=&quot;{{ icon_classes.search }}&quot;</code>. These are strings specifying CSS classes used for rendering icons. I pass these classes to the form in <code>icon_classes</code> variable from form view.</p>
<p>NetBox v2.10+ uses Material Design icons. Previous versions used Font Awesome. To make my plugin compatible with both versions I created <code>icon_classes.py</code> file with dictionary dynamically mapping class names to underlying MD or FA CSS classes.</p>
<pre><code># icon_classes.py
from .release import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_210


if NETBOX_RELEASE_CURRENT &gt;= NETBOX_RELEASE_210:
    icon_classes = {
        &quot;plus&quot;: &quot;mdi mdi-plus-thick&quot;,
        &quot;search&quot;: &quot;mdi mdi-magnify&quot;,
        &quot;remove&quot;: &quot;mdi mdi-close-thick&quot;,
    }
else:
    icon_classes = {
        &quot;plus&quot;: &quot;fa fa-plus&quot;,
        &quot;search&quot;: &quot;fa fa-search&quot;,
        &quot;remove&quot;: &quot;fa fa-remove&quot;,
    }
</code></pre>
<p>I also use <code>release.py</code> borrowed from <sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup> to detect version of NetBox plugin is running under.</p>
<pre><code>from packaging import version
from django.conf import settings

NETBOX_RELEASE_CURRENT = version.parse(settings.VERSION)
NETBOX_RELEASE_28 = version.parse(&quot;2.8&quot;)
NETBOX_RELEASE_29 = version.parse(&quot;2.9&quot;)
NETBOX_RELEASE_210 = version.parse(&quot;2.10&quot;)
</code></pre>
<p>The above additions allow me to easily add CSS classes for any other icons I might want to use in the future.</p>
<p>With that digression out of the way, let's put it all together by modifying list view class.</p>
<p><a name="adding-filtering"></a></p>
<h2 id="addingfilteringtolistviewclass">Adding filtering to list view class</h2>
<p>All of the components we created up to this point need to be tied together in the class view. We're modifying the <code>BgpPeeringListView</code> class created in part 2, in order to support search/filtering functionality.</p>
<p>Changed <code>BgpPeeringListView</code> class:</p>
<pre><code># views.py
from .icon_classes import icon_classes
from .filters import BgpPeeringFilter
from .forms import BgpPeeringForm, BgpPeeringFilterForm

class BgpPeeringListView(View):
    &quot;&quot;&quot;View for listing all existing BGP Peerings.&quot;&quot;&quot;

    queryset = BgpPeering.objects.all()
    filterset = BgpPeeringFilter
    filterset_form = BgpPeeringFilterForm

    def get(self, request):
        &quot;&quot;&quot;Get request.&quot;&quot;&quot;

        self.queryset = self.filterset(request.GET, self.queryset).qs

        table = BgpPeeringTable(self.queryset)
        RequestConfig(request, paginate={&quot;per_page&quot;: 25}).configure(table)

        return render(
            request,
            &quot;netbox_bgppeering/bgppeering_list.html&quot;,
            {
                &quot;table&quot;: table,
                &quot;filter_form&quot;: self.filterset_form(request.GET),
                &quot;icon_classes&quot;: icon_classes,
            },
        )
</code></pre>
<p>Few interesting things happen here, let's break them down.</p>
<ul>
<li>
<p><code>filterset = BgpPeeringFilter</code> - We add <code>filterset</code> attribute and set it to <code>BgpPeeringFilter</code> class we created earlier.</p>
</li>
<li>
<p><code>filterset_form = BgpPeeringFilterForm</code> - This is the form that will be rendered in list view template.</p>
</li>
<li>
<p><code>self.queryset = self.filterset(request.GET, self.queryset).qs</code> - Here is where the filtering happens.<br>
We feed <code>filterset</code> form values contained in <code>request.GET</code> and <code>queryset</code> with BgpPeering objects.<br>
Method <code>.qs</code> returns <code>QuerySet</code> like object that we assign back to <code>self.queryset</code>. This will be then fed to table constructor. Except now the resulting table will only contain objects matching filter values.</p>
</li>
</ul>
<p>Finally, we provide two new arguments to <code>render</code>:</p>
<ul>
<li>
<p><code>&quot;filter_form&quot;: self.filterset_form(request.GET)</code> - This is used to render form in UI. <code>request.GET</code> preserves values used in previous searches.</p>
</li>
<li>
<p><code>&quot;icon_classes&quot;: icon_classes</code> - This passes dictionary with CSS classes I defined for UI icons.</p>
</li>
</ul>
<p>And that's it. We can now re-build plugin and take search panel for a spin.</p>
<p>If you now navigate to <code>/plugins/bgp-peering/</code> you should see search panel on the right hand side, next to table with the list of objects.</p>
<p><img src="https://ttl255.com/content/images/2021/01/search-init.png" alt="search-init"></p>
<p>And here's the table after we asked for peerings on one device only.<br>
<img src="https://ttl255.com/content/images/2021/01/device-filter.png" alt="device-filter"></p>
<p>And here's a result of the general query for <code>primary</code> string.<br>
<img src="https://ttl255.com/content/images/2021/01/search-gquery.png" alt="search-gquery"></p>
<p>All working as intended, pretty cool right?</p>
<p>Source code with all the modifications we made up to this point is in branch <code>bgppeering-list-search</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-list-search" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-list-search</a></p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>This concludes post 3 in Developing NetBox Plugin series. Search functionality we implemented here should greatly improve usability of our plugin. It's going to be much easier now to find peering objects of interest.</p>
<p>In the next post we'll continue with making improvements to the plugin. There a few small things here and there that will make this all even better. Stay tuned!</p>
<p><a name="resources"></a></p>
<h2 id="resources">Resources</h2>
<ul>
<li>NetBox docs: Plugin Development: <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></li>
<li>NetBox source code on GitHub: <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a></li>
<li>NetBox Extensibility Overview, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=FSoCzuWOAE0" target="_blank">https://www.youtube.com/watch?v=FSoCzuWOAE0</a></li>
<li>NetBox Plugins Development, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=LUCUBPrTtJ4" target="_blank">https://www.youtube.com/watch?v=LUCUBPrTtJ4</a></li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Django Filter app - <a href="https://django-filter.readthedocs.io/en/stable/" target="_blank">https://django-filter.readthedocs.io/en/stable/</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Django field lookups - <a href="https://docs.djangoproject.com/en/3.1/ref/models/querysets/#field-lookups" target="_blank">https://docs.djangoproject.com/en/3.1/ref/models/querysets/#field-lookups</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Django filter method - <a href="https://django-filter.readthedocs.io/en/stable/ref/filters.html#method" target="_blank">https://django-filter.readthedocs.io/en/stable/ref/filters.html#method</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Django Q object - <a href="https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q" target="_blank">https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>NTC NetBox Onboarding plugin - <a href="https://github.com/networktocode/ntc-netbox-plugin-onboarding/blob/develop/netbox_onboarding/release.py" target="_blank">https://github.com/networktocode/ntc-netbox-plugin-onboarding/blob/develop/netbox_onboarding/release.py</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Developing NetBox Plugin - Part 2 - Adding web UI pages]]></title><description><![CDATA[Learn how to build your own NetBox plugins. In this post I show you how to add web UI pages to BgpPeering plugin.]]></description><link>https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/</link><guid isPermaLink="false">5fdddbc11e69ff52c5b0663d</guid><category><![CDATA[netbox]]></category><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[tools]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 20 Dec 2020 17:14:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Welcome to part 2 of my tutorial walking you through process of developing NetBox plugin. In part 1 we set up our development environment and built base version of Bgp Peering plugin. Here we will continue work on the plugin by adding UI pages allowing us to list, view and add, Bgp Peering objects.</p>
<h2 id="developingnetboxplugintutorialseries">Developing NetBox Plugin tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/">Developing NetBox Plugin - Part 1 - Setup and initial build</a></li>
<li>Developing NetBox Plugin - Part 2 - Adding web UI pages</li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-3-adding-search/">Developing NetBox Plugin - Part 3 - Adding search panel</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/">Developing NetBox Plugin - Part 4 - Small improvements</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/">Developing NetBox Plugin - Part 5 - Permissions and API</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#adding-web-ui-pa">Adding web UI pages</a></li>
<li><a href="#enable-model-for">Enable model for permissions framework</a></li>
<li><a href="#single-object-vi">Single object view - <code>BgpPeeringView</code></a>
<ul>
<li><a href="#template-for-sin">Template for single object view</a></li>
<li><a href="#url-for-single-o">URL for single object view</a></li>
</ul>
</li>
<li><a href="#object-list-view">Object list view - <code>BgpPeeringListView</code></a>
<ul>
<li><a href="#create-table-cla">Create table class</a></li>
<li><a href="#template-for-obj">Template for object list view</a></li>
<li><a href="#object-list-view">Object list view class</a></li>
<li><a href="#adding-get-absol">Adding <code>get_absolute_url</code> to model</a></li>
</ul>
</li>
<li><a href="#object-creation">Object creation view - <code>BgpPeeringCreateView</code></a>
<ul>
<li><a href="#form-for-object">Form for object creation view</a></li>
<li><a href="#object-creation">Object creation view class</a></li>
<li><a href="#template-for-obj">Template for object creation view</a></li>
<li><a href="#navigation-butto">Navigation button for object creation</a></li>
<li><a href="#url-for-object-c">URL for object creation view</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#resources">Resources</a></li>
<li><a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering" target="_blank">BGP Peering Plugin repository</a></li>
</ul>
<h2 id="disclaimer">Disclaimer</h2>
<p>Original version of this post contained views that relied on views used internally by NetBox. This practice is <strong>not recommended</strong> by NetBox maintainers as these are likely to change in the future and your plugin might stop working.</p>
<p>To add to it, bugs with plugins are often logged against the core NetBox adding to the mainteners' workload even though this has nothing to do with them. I've now refactored these views to decouple this plugin from NetBox's implementation details.</p>
<p><a name="adding-web-ui-pa"></a></p>
<h2 id="addingwebuipages">Adding web UI pages</h2>
<p>We previously created <code>BgpPeering</code> model which we can even work with from admin panel. This was all for quick testing and we should now expose the model to the end users.</p>
<p>The best way to do that is by adding set of pages to web UI. We're also going to re-use some of the NetBox's components so that our pages match look and feel of core offering.</p>
<p>Here are some of the things we need to work on to make this happen.</p>
<ol>
<li>Create template for pages, these go into in <code>templates</code> directory.</li>
<li>Write code responsible for rendering web response, we store views in <code>views.py</code> file.</li>
<li>Add URLs pointing to our new pages in <code>urls.py</code></li>
<li>Add menu items in <code>navigation.py</code> where appropriate.</li>
<li>Create table class in <code>tables.py</code> for list view.</li>
</ol>
<p>You will see me talking about views in few places. If you've never heard the term before, know that view<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> in Django is Python code that receives web request, executes some logic and returns web response.</p>
<p>In this post I want to create three views:</p>
<ul>
<li>view displaying details of single object</li>
<li>view displaying list of objects</li>
<li>view with form for adding new object</li>
</ul>
<p>But before we get to views there is one more thing that we need to do. We need to make our model compatible with NetBox's permissions framework.</p>
<p><a name="enable-model-for"></a></p>
<h2 id="enablemodelforpermissionsframework">Enable model for permissions framework</h2>
<p>NetBox has support for granular query permissions. Even though we currently don't use any permissions we want our components to support them. This will make future additions easier to make.</p>
<p>Below is the code I'm adding to <code>models.py</code>.</p>
<pre><code>from utilities.querysets import RestrictedQuerySet

class BgpPeering(ChangeLoggedModel):
  ...
  objects = RestrictedQuerySet.as_manager()
</code></pre>
<p>We create class attribute called <code>objects</code> and assign <code>RestrictedQuerySet</code> to it. This will be used for retrieving and filtering <code>BgpPeering</code> records.</p>
<p><code>RestrictedQuerySet</code> is a class we import from NetBox's utilities. This class provides support for permissions.</p>
<p>By using <code>RestrictedQuerySet</code> NetBox will be able to filter out objects for which user does not have specific rights.</p>
<p><code>objects</code> is a default name Django uses for db query interface, <code>Manager</code>. Follow hyperlink to footnotes if you want to learn more <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<p>Finally <code>as_manager()</code> method is used here to return instance of <code>Manager</code> class. This is a bit low level but know that Django expects to see <code>Manager</code> type here. We used custom <code>QuerySet</code> which <code>as_manager()</code> allows to use as <code>Manager</code>.</p>
<p>With that out of the way we can move to our first view.</p>
<p><a name="single-object-vi"></a></p>
<h2 id="singleobjectviewbgppeeringview">Single object view - <code>BgpPeeringView</code></h2>
<p>First view we're going to create has name <code>BgpPeeringView</code>. Choice of names is completely arbitrary but I'm trying to follow naming convention used by NetBox.</p>
<p>This and all other views have to go into <code>views.py</code> file in your plugin's directory.</p>
<p>Also, for our views we'll be using Django class based views. These allow more flexiblity and reuse of code via inheritance compared to function based views<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>Below is the initial code, which we're going to breakdown in a second.</p>
<pre><code># views.py
from django.shortcuts import get_object_or_404, render
from django.views import View

from .models import BgpPeering


class BgpPeeringView(View):
    &quot;&quot;&quot;Display BGP Peering details&quot;&quot;&quot;

    queryset = BgpPeering.objects.all()

    def get(self, request, pk):
        &quot;&quot;&quot;Get request.&quot;&quot;&quot;
        bgppeering_obj = get_object_or_404(self.queryset, pk=pk)

        return render(
            request,
            &quot;netbox_bgppeering/bgppeering.html&quot;,
            {
                &quot;bgppeering&quot;: bgppeering_obj,
            },
        )
</code></pre>
<p>Let's have a closer look at this code.</p>
<ul>
<li>
<p>We subclass <code>View</code> which comes from core Django. This is one of the most basic type of class-based views.</p>
</li>
<li>
<p>Next we have <code>queryset</code> which we use to retrieve and filter interesting objects. We do this by calling method <code>all()</code> on <code>objects</code> attribute we just defined in <code>BgpPeering</code> model. No database calls are made at this stage so don't be alarmed by use of <code>all()</code>.</p>
</li>
<li>
<p>Method <code>get()</code> is used to service incoming GET HTTP requests.</p>
</li>
<li>
<p>Function <code>get_object_or_404()</code> returns <code>404</code> HTML code instead of raising internal exception. This is more meaningful to end users.</p>
</li>
<li>
<p>Then we're feeding defined queryset to <code>get_object_or_404</code> asking for single object matching <code>pk</code>.  <code>pk</code> means primary key and each of objects will have one.<br>
In our case <code>pk</code> matches automatically genereated <code>id</code> field in our model. The value of <code>pk</code> is passed to <code>get()</code> via URL defined in <code>urls.py</code> which we'll look at shortly.</p>
</li>
<li>
<p>Finally <code>render()</code> renders provided template and uses it to create a well-formed web response. We provide it name of the template to render, which we're going to build next.</p>
</li>
</ul>
<p><a name="template-for-sin"></a></p>
<h3 id="templateforsingleobjectview">Template for single object view</h3>
<p>Most of our views will rely on templates stored in <code>templates</code> directory. Django templates use language<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup> similar to Jinja2 so if you know Jinja you should be able to pick it up pretty quickly.</p>
<p>Best practice is to place templates used by our plugin in <code>templates</code> subdirectory named after our plugin. In our case that will be:</p>
<p><code>templates\bgppeering\</code></p>
<p>We refer to these templates later in few places. This makes it clear that template came from plugin namespace.</p>
<p>With that said, let's have a look at body of our template.</p>
<p><code>bgppeering.html</code></p>
<pre><code class="language-html">{% extends 'base.html' %}
{% load helpers %}

{% block content %}
&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-6 col-md-offset-3&quot;&gt;
        &lt;div class=&quot;panel panel-default&quot;&gt;
            &lt;div class=&quot;panel-heading&quot;&gt;
                &lt;strong&gt;BGP Peering&lt;/strong&gt;
            &lt;/div&gt;
            &lt;table class=&quot;table table-hover panel-body attr-table&quot;&gt;
                &lt;tr&gt;
                    &lt;td&gt;Site&lt;/td&gt;
                    &lt;td&gt;
                        {% if bgppeering.site %}
                            &lt;a href=&quot;{% url 'dcim:site' slug=bgppeering.site.slug %}&quot;&gt;{{ bgppeering.site }}&lt;/a&gt;
                        {% else %}
                            &lt;span class=&quot;text-muted&quot;&gt;None&lt;/span&gt;
                        {% endif %}
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Device&lt;/td&gt;
                    &lt;td&gt;
                        &lt;a href=&quot;{% url 'dcim:device' pk=bgppeering.device.pk %}&quot;&gt;{{ bgppeering.device }}&lt;/a&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Local BGP AS&lt;/td&gt;
                    &lt;td&gt;{{ bgppeering.local_as }}&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Local peering IP address&lt;/td&gt;
                    &lt;td&gt;
                        &lt;a href=&quot;{% url 'ipam:ipaddress' pk=bgppeering.local_ip.pk %}&quot;&gt;{{ bgppeering.local_ip }}&lt;/a&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Remote BGP AS&lt;/td&gt;
                    &lt;td&gt;{{ bgppeering.remote_as }}&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Remote peering IP address&lt;/td&gt;
                    &lt;td&gt;{{ bgppeering.remote_ip }}&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Peer name&lt;/td&gt;
                    &lt;td&gt;{{ bgppeering.peer_name|placeholder }}&lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Description&lt;/td&gt;
                    &lt;td&gt;{{ bgppeering.description|placeholder }}&lt;/td&gt;
                &lt;/tr&gt;
            &lt;/table&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
{% endblock %}
</code></pre>
<p>This looks like a lot but most of it is code generating table cells displaying attributes of object.</p>
<p>There are some interesting bits here though, let's look at them now:</p>
<ul>
<li>
<p><code>{% extends 'base.html' %}</code> - <code>base.html</code>comes from NetBox and takes care of NetBox's look and feel for our page. It'll give us menus, footer, etc., we just need to take care of the main content.</p>
</li>
<li>
<p><code>{% load helpers %}</code> - loads custom tags and filters defined in <code>helpers</code>. Again, we borrow <code>helpers</code> from core NetBox. We need this because of <code>placeholder</code> filter used in our template.</p>
</li>
<li>
<p>Below links pointing to NetBox objects use Django's <code>url</code> filter<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>. With that filter we don't have to hardcode links, instead we reference paths in <code>urls.py</code>.</p>
<pre><code>&lt;a href=&quot;{% url 'dcim:site' slug=bgppeering.site.slug %}&quot;&gt;{{ bgppeering.site }}&lt;/a&gt;
&lt;a href=&quot;{% url 'dcim:device' pk=bgppeering.device.pk %}&quot;&gt;{{ bgppeering.device }}&lt;/a
&lt;a href=&quot;{% url 'ipam:ipaddress' pk=bgppeering.local_ip.pk %}&quot;&gt;{{ bgppeering.local_ip }}&lt;/a&gt;
</code></pre>
<p>For instance:</p>
<pre><code>&lt;a href=&quot;{% url 'dcim:site' slug=bgppeering.site.slug %}&quot;&gt;{{ bgppeering.site }}&lt;/a&gt;
</code></pre>
<p>Points to below path in <code>netbox/netbox/dcim/views.py</code>:</p>
<p><code>path('sites/&lt;slug:slug&gt;/', views.SiteView.as_view(), name='site')</code></p>
<p>Because URL is in <code>dcim</code> app and has <code>name</code> equal to <code>site</code> we feed <code>dcim:site</code> to <code>url</code> filter. This path also expects <code>slug</code> argument. <code>BgpPeering</code>object keeps site info in <code>site</code> attribute, so the site <code>slug</code> can be retrieved with <code>bgppeering.site.slug</code>.</p>
<p>If you want to link to any other NetBox objects you can look at the paths recorded in <code>urls.py</code> for given app. Then you need to identify expected argument. With those two you can construct links using <code>url</code> filter, just like we did above.</p>
</li>
<li>
<p>Lastly we display attribute values for given <code>BgpPeering</code> instance by using dot <code>.</code> notation.<br>
In view we created earlier, template receives variable named <code>bgppeering</code> containing <code>BgpPeering</code> object retrieved from database. Inside of our template we use that name to retrieve each of the model attributes by placing <code>.</code> after <code>bgppeering</code>, followed by name of the attribute. E.g.</p>
<pre><code class="language-text">bgppeering.site
bgppeering.remote_ip
</code></pre>
</li>
</ul>
<p><a name="url-for-single-o"></a></p>
<h3 id="urlforsingleobjectview">URL for single object view</h3>
<p>We have single object view and template in place. Now we need to add URL path for it so the view can be accessed.</p>
<pre><code># urls.py
from .views import BgpPeeringView

urlpatterns = [
    ...
    path(&quot;&lt;int:pk&gt;/&quot;, BgpPeeringView.as_view(), name=&quot;bgppeering&quot;),
]
</code></pre>
<p>First we import our view <code>BgpPeeringView</code>. Then we add another path entry to <code>urlpatterns</code>.</p>
<ul>
<li>
<p><code>&lt;int:pk&gt;/</code> equals to <code>plugins/bgp-peering/&lt;pk&gt;</code>, where <code>pk</code> is the primary key of our record, and it's an integer, hence <code>int</code>. Our object use auto-incremented integer <code>id</code> field as primary-key. Below is example of URL for object with <code>id</code> equal to 1.</p>
<p><code>plugins/bgp-peering/1/</code></p>
</li>
<li>
<p><code>BgpPeering.as_view()</code> - <code>as_view()</code> is needed here so that our view class can process requests. Technically speaking this creates callable that takes request and returns well- formed response. This is how we're going to use all our class based views.</p>
</li>
</ul>
<p>The end result of rendering this template is basic but clean looking table presenting details of BgpPeering object:</p>
<p><img src="https://ttl255.com/content/images/2020/12/bgp-peering-view-init.png" alt="bgp-peering-view-init"></p>
<p>There are some improvements that we could make to this view but we'll leave that for later.</p>
<p>Source code up to this point is in branch <code>bgppeering-view-init</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-view-init" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-view-init</a> .</p>
<p><a name="object-list-view"></a></p>
<h2 id="objectlistviewbgppeeringlistview">Object list view - <code>BgpPeeringListView</code></h2>
<p>List view is usually the default view for objects in NetBox. This provides overview of records for object of given type and includes links to detailed views.</p>
<p>We're now going to create list view for records using <code>BgpPeering</code> model. This view will be the one we link to from navigational menu we created in post 1.</p>
<p><a name="create-table-cla"></a></p>
<h3 id="createtableclass">Create table class</h3>
<p>To create list page we'll start with building table class.</p>
<p>What's a table class you ask? NetBox uses Django app called <code>django_tables2</code> <sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup> to make working with tables easier. We'll save ourselves some work by following NetBox's example and leveraging that package.</p>
<p>To do that we need to create file called <code>tables.py</code> in our plugin package and add code defining our table.</p>
<pre><code># tables.py
import django_tables2 as tables
from utilities.tables import BaseTable
from .models import BgpPeering


class BgpPeeringTable(BaseTable):
    &quot;&quot;&quot;Table for displaying BGP Peering objects.&quot;&quot;&quot;

    id = tables.LinkColumn()
    site = tables.LinkColumn()
    device = tables.LinkColumn()
    local_ip = tables.LinkColumn()

    class Meta(BaseTable.Meta):
        model = BgpPeering
        fields = (
            &quot;id&quot;,
            &quot;site&quot;,
            &quot;device&quot;,
            &quot;local_ip&quot;,
            &quot;peer_name&quot;,
            &quot;remote_ip&quot;,
            &quot;remote_as&quot;,
        )
</code></pre>
<p>Let's break this code down.</p>
<ul>
<li>
<p>Our table is a class, named <code>BgpPeeringTable</code>. We subclass <code>BaseTable</code> from NetBox utilities which adds some NetBox specific stuff.</p>
</li>
<li>
<p>In class <code>Meta</code> we define model used in the table, followed by names of the fields we want displayed in the table. This class needs to subclass <code>BaseTable.Meta</code>. Few things of note:</p>
<ul>
<li>You don't have to list all of the fields used by your model. You should select whatever you feel makes sense to show as one line summary for each object.</li>
<li>Order in which fields are displayed on the page matches order in which you listed them.</li>
</ul>
</li>
<li>
<p>Finally we define class attributes for fields that need special treatment. We list several fields here:</p>
<ul>
<li><code>id</code>, <code>site</code>, <code>device</code> and <code>local_ip</code> use <code>tables.LinkColumn()</code> object. This will  give us auto-generated links pointing to corresponding NetBox objects.</li>
</ul>
</li>
</ul>
<p>With that in place we're moving to the template.</p>
<p><a name="template-for-obj"></a></p>
<h3 id="templateforobjectlistview">Template for object list view</h3>
<p>Compared to template for single view, this one is much shorter because we're offloading a lot of work. Let's have a look at the body of the template and then we'll break it down.</p>
<p><code>bgppeering_list.html</code></p>
<pre><code class="language-html">{% extends 'base.html' %}
{% load render_table from django_tables2 %}

{% block content %}
&lt;h1&gt;{% block title %}BGP Peerings{% endblock %}&lt;/h1&gt;
&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-9&quot;&gt;
        {% render_table table %}
    &lt;/div&gt;
&lt;/div&gt;
{% endblock %}
</code></pre>
<p>We are again extending from <code>base.html</code>. Then we have <code>content</code> block which contains suspiciously little amount of code.</p>
<p>There is <code>title</code> block, in which we override block in <code>base.html</code> with the same name. Then we just have <code>render_table table</code> statement inside of div.</p>
<p>That <code>render_table</code><sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup> statement is where a lot of heavy lifting happens. It's template tag that comes from <code>django_tables2</code> and it renders HTML table for us among other things. All we have to do is provide this template our previously defined table class in variable called <code>table</code>.</p>
<p><a name="object-list-view"></a></p>
<h3 id="objectlistviewclass">Object list view class</h3>
<p>Finally we have view class. Our class will this time inherit from <code>ObjectListView</code> class coming from NetBox. That class provides a lot of goodness for building views for series of objects, and it will use table class we built earlier.</p>
<p>We're updating our <code>views.py</code> with the following additions.</p>
<pre><code># views.py
from django_tables2 import RequestConfig
...
class BgpPeeringListView(View):
    &quot;&quot;&quot;View for listing all existing BGP Peerings.&quot;&quot;&quot;

    queryset = BgpPeering.objects.all()

    def get(self, request):
        &quot;&quot;&quot;Get request.&quot;&quot;&quot;
        table = BgpPeeringTable(self.queryset)
        RequestConfig(request, paginate={&quot;per_page&quot;: 25}).configure(table)

        return render(
            request, &quot;netbox_bgppeering/bgppeering_list.html&quot;, {&quot;table&quot;: table}
        )
</code></pre>
<p>We named this view class <code>BgpPeeringListView</code>. Inside the class we have queryset where we specify that we want all of the objects to be given to the view.</p>
<p>Then we have our table class in <code>table</code> var and <code>RequestConfig</code> object. We use request to configure pagination of 25 object per page with:</p>
<p><code>RequestConfig(request, paginate={&quot;per_page&quot;: 25}).configure(table)</code></p>
<p>Finally we call <code>render</code> to return well formed web response. We give it <code>request</code> object, name of the template, and <code>table</code> object used in our template.</p>
<p>We're almost done here, but there's one more thing we need to do before we try this view out.</p>
<p><a name="adding-get-absol"></a></p>
<h3 id="addingget_absolute_urltomodel">Adding <code>get_absolute_url</code> to model</h3>
<p>For list view to work we have to implement method <code>get_absolute_url</code><sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup> in <code>BgpPeering</code> model. This is required by list view to automatically create links to details of <code>BgpPeering</code> objects.</p>
<p>If you remember, in the table class we made <code>id</code> a <code>LinkedColumn</code> with <code>id = tables.LinkColumn()</code>. Now we need to add some code to <code>BgpPeering</code> model for this to actually work.</p>
<p>Add below to <code>models.py</code>.</p>
<pre><code># models.py
from django.urls import reverse

  class BgpPeering(ChangeLoggedModel):
  ...
    def get_absolute_url(self):
        &quot;&quot;&quot;Provide absolute URL to a Bgp Peering object.&quot;&quot;&quot;
        return reverse(&quot;plugins:netbox_bgppeering:bgppeering&quot;, kwargs={&quot;pk&quot;: self.pk})
</code></pre>
<p>We defined <code>get_absolute_url</code> method that has single line of code.</p>
<p>In that line <code>reverse</code> function will generate correct URL for given BgpPeering record based on the provided <code>pk</code>. We use the name defined in <code>urls</code> to point to correct path mapped to single object view.</p>
<p>And that's it, we're ready to try out our list view.</p>
<p>If you now click on <code>BGP Peerings</code> in Plugins menu, you should get list view.</p>
<p><img src="https://ttl255.com/content/images/2020/12/bgp-peering-list-init2.png" alt="bgp-peering-list-init2"></p>
<p>Notice auto-generated URLs to linked objects. We also get pagination for free!</p>
<p>Source code up to this point is in branch <code>bgppeering-list-view-init</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-list-view-init" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-list-view-init</a> .</p>
<p><a name="object-creation"></a></p>
<h2 id="objectcreationviewbgppeeringcreateview">Object creation view - <code>BgpPeeringCreateView</code></h2>
<p>We could already create objects from admin panel but that is not available to regular users.</p>
<p>Time to rectify this oversight and create view for adding <code>BgpPeering</code> objects.</p>
<p><a name="form-for-object"></a></p>
<h3 id="formforobjectcreationview">Form for object creation view</h3>
<p>For creation view to work we need to build class representing creation form. This is the form that end user will have to fill out when adding new object.</p>
<p>Forms go into <code>forms.py</code> file in the plugin's directory.</p>
<p>This is what I've added to that file.</p>
<pre><code># forms.py
from django import forms

from utilities.forms import BootstrapMixin

from .models import BgpPeering


class BgpPeeringForm(BootstrapMixin, forms.ModelForm):
    &quot;&quot;&quot;Form for creating a new BgpPeering object.&quot;&quot;&quot;

    class Meta:
        model = BgpPeering
        fields = [
            &quot;site&quot;,
            &quot;device&quot;,
            &quot;local_as&quot;,
            &quot;local_ip&quot;,
            &quot;peer_name&quot;,
            &quot;remote_as&quot;,
            &quot;remote_ip&quot;,
            &quot;description&quot;,
        ]
</code></pre>
<ul>
<li>
<p>We create class <code>BgpPeeringForm</code> subclassing <code>BootstrapMixin</code> and <code>forms.ModelForm</code>.</p>
</li>
<li>
<p><code>forms.ModelForm</code><sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup> is a Django helper class that allows building forms from models. <code>BootstrapMixin</code> comes from NetBox and adds Bootstrap CSS classes. This makes our form match the look and feel of other forms used in NetBox.</p>
</li>
<li>
<p>In form class itself we define <code>Meta</code> class where we:</p>
<ul>
<li>specify model used to generate the form with <code>model = BgpPeering</code></li>
<li>list fields that will show up on the form in list assigned to <code>fields</code> variable.</li>
</ul>
</li>
</ul>
<p>And that's it, we're ready to create the view class.</p>
<p><a name="object-creation"></a></p>
<h3 id="objectcreationviewclass">Object creation view class</h3>
<p>With form in place we can now build a view.</p>
<p>Let's add the below code to <code>views.py</code>:</p>
<pre><code># views.py
...
from django.views.generic.edit import CreateView
...
from .forms import BgpPeeringForm

...

class BgpPeeringCreateView(CreateView):
    &quot;&quot;&quot;View for creating a new BgpPeering instance.&quot;&quot;&quot;

    form_class = BgpPeeringForm
    template_name = &quot;netbox_bgppeering/bgppeering_edit.html&quot;
</code></pre>
<p>We again create a class representing our view. But here we inherit from <code>CreateView</code> provided by Django. This helps us offload boilerplate related to validation and saving.</p>
<p>Form class we created in <code>forms.py</code> gets assigned to <code>form_class</code> variable. This will be used in the template.</p>
<p>To top it off we specify template we want to use for this form.</p>
<pre><code>template_name = &quot;netbox_bgppeering/bgppeering_edit.html&quot;
</code></pre>
<p>Great, but we don't have that template yet you say. Indeed, time to create it.</p>
<p><a name="template-for-obj"></a></p>
<h3 id="templateforobjectcreationview">Template for object creation view</h3>
<p>We create new template and save it as <code>bgppeering_edit.html</code>.</p>
<pre><code>{% extends 'base.html' %}

{% block content %}
&lt;form action=&quot;&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot; class=&quot;form form-horizontal&quot;&gt;
    {% csrf_token %}
    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-md-6 col-md-offset-3&quot;&gt;
            &lt;h3&gt;
                {% block title %}Add a new BGP Peering{% endblock %}
            &lt;/h3&gt;
            &lt;div class=&quot;panel panel-default&quot;&gt;
                &lt;div class=&quot;panel-heading&quot;&gt;&lt;strong&gt;BGP Peering&lt;/strong&gt;&lt;/div&gt;
                &lt;div class=&quot;panel-body&quot;&gt;
                    {% for field in form %}
                    &lt;div class=&quot;form-group&quot;&gt;
                        &lt;label class=&quot;col-md-3 control-label {% if field.field.required %} required{% endif %}&quot; for=&quot;{ field.id_for_label }}&quot;&gt;
                            {{ field.label }}
                        &lt;/label&gt;
                        &lt;div class=&quot;col-md-9&quot;&gt;
                            {{ field }}
                            {% if field.help_text %}
                            &lt;span class=&quot;help-block&quot;&gt;{{ field.help_text|safe }}&lt;/span&gt;
                            {% endif %}
                        &lt;/div&gt;
                    &lt;/div&gt;
                    {% endfor %}
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col-md-6 col-md-offset-3 text-right&quot;&gt;
            {% block buttons %}
            &lt;button type=&quot;submit&quot; name=&quot;_create&quot; class=&quot;btn btn-primary&quot;&gt;Create&lt;/button&gt;
            {% endblock %}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/form&gt;
{% endblock %}
</code></pre>
<p>Looks like there's a lot going on but it's not that scary actually.</p>
<p>First we define HTML <code>form</code>. Inside of the form we add <code>div</code> elements making this form centered on the page.</p>
<p>Then we loop over fields of the form with <code>{% for field in form %}</code>. For each field we display label, in bold if field is required. Then we show the field itself.</p>
<p>Django will match render of each field to its type, as defined in the model. Finally we display helper text if one exists.</p>
<p>Validation and creation of the object will be handled by Django, courtesy of the class we're subclassing. After object is created we will be redirected to the single object view.</p>
<p>We're almost there, we only have two small additions left and create view will be ready for action.</p>
<p><a name="navigation-butto"></a></p>
<h3 id="navigationbuttonforobjectcreation">Navigation button for object creation</h3>
<p>So we've got our form and logic behind it but we need to access it somehow. For that we will add green plus button next to <code>BGP Peerings</code> entry in navigation bar. This will match behavior of the other NetBox menu items.</p>
<p>Below is the <code>navigation.py</code> after additions.</p>
<pre><code># navigation.py
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices


menu_items = (
    PluginMenuItem(
        link=&quot;plugins:netbox_bgppeering:bgppeering_list&quot;,
        link_text=&quot;BGP Peerings&quot;,
        buttons=(
            PluginMenuButton(
                link=&quot;plugins:netbox_bgppeering:bgppeering_add&quot;,
                title=&quot;Add&quot;,
                icon_class=&quot;fa fa-plus&quot;,
                color=ButtonColorChoices.GREEN,
            ),
        ),
    ),
)
</code></pre>
<p>We are passing extra items to <code>buttons</code> argument of <code>PluginMenuItem</code>.</p>
<p>Class needed for creating button is called <code>PluginMenuButton</code> and we initialize it with few arguments:</p>
<ul>
<li><code>link</code> - this needs to match name of the path for our create view. We're going to add this path in <code>urls.py</code> shortly.</li>
<li><code>title</code> - Text that appears when you hover over the button.</li>
<li><code>icon_class</code> - specifies font-awesome icon to use <code>fa fa-plus</code> is a plus sign.</li>
<li><code>color</code> - color of our button <code>ButtonColorChoices.GREEN</code> is green.</li>
</ul>
<p>The end result should look like this:</p>
<p><img src="https://ttl255.com/content/images/2020/12/add-button.png" alt="add-button"></p>
<p><a name="url-for-object-c"></a></p>
<h3 id="urlforobjectcreationview">URL for object creation view</h3>
<p>Finally, we need URL leading to object creation form. Let's modify <code>urls.py</code>.</p>
<pre><code># urls.py
from .views import BgpPeeringCreateView, BgpPeeringView, BgpPeeringListView


urlpatterns = [
    ...
    path(&quot;add/&quot;, BgpPeeringCreateView.as_view(), name=&quot;bgppeering_add&quot;),
]
</code></pre>
<p>We import <code>BgpPeeringCreateView</code> and register it under <code>add/</code> path using name <code>bgppeering_add</code>.</p>
<p>And that's it. With all the components in place we're ready to take our form for a spin!</p>
<p><img src="https://ttl255.com/content/images/2020/12/bgp-peering-add-init-new.png" alt="bgp-peering-add-init-new"></p>
<p>And here it is, form for adding BGP Peering object in its full glory!</p>
<p>Source code up to this point is in branch <code>bgppeering-create-view-init</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-create-view-init" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/bgppeering-create-view-init</a> .</p>
<p><a name="conclusion"></a></p>
<h3 id="conclusion">Conclusion</h3>
<p>This concludes post 2 in Developing NetBox Plugin series. Web UI pages we built here give our users ways of interacting with model we built in part 1. They can now list, view details of, and create new Bgp Peering instances.</p>
<p>We could really stop here but there's more we can add to our pages. We can integrate change log into views. Or why not make our views like other NetBox pages by adding search panel and add/edit/delete buttons. How about permissions? It'd be good to revisit those and make our plugin use them.</p>
<p>We will be looking at some of those improvements in the future posts. So do come back for more!</p>
<p><a name="resources"></a></p>
<h2 id="resources">Resources</h2>
<ul>
<li>NetBox docs: Plugin Development: <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></li>
<li>NetBox source code on GitHub: <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a></li>
<li>NetBox Extensibility Overview, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=FSoCzuWOAE0" target="_blank">https://www.youtube.com/watch?v=FSoCzuWOAE0</a></li>
<li>NetBox Plugins Development, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=LUCUBPrTtJ4" target="_blank">https://www.youtube.com/watch?v=LUCUBPrTtJ4</a></li>
</ul>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Django views: <a href="https://docs.djangoproject.com/en/3.1/topics/http/views/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/http/views/</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Django <code>Manager</code>: <a href="https://docs.djangoproject.com/en/3.1/topics/db/managers/#django.db.models.Manager" target="_blank">https://docs.djangoproject.com/en/3.1/topics/db/managers/#django.db.models.Manager</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Django class-based Views: <a href="https://docs.djangoproject.com/en/3.1/topics/class-based-views/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/class-based-views/</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Django templating language: <a href="https://docs.djangoproject.com/en/3.1/ref/templates/language/" target="_blank">https://docs.djangoproject.com/en/3.1/ref/templates/language/</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Django <code>url</code> filter: <a href="https://docs.djangoproject.com/en/3.1/ref/templates/builtins/#url" target="_blank">https://docs.djangoproject.com/en/3.1/ref/templates/builtins/#url</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>django-tables2  docs: <a href="https://django-tables2.readthedocs.io/en/latest/" target="_blank">https://django-tables2.readthedocs.io/en/latest/</a> <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn7" class="footnote-item"><p><code>render-table</code> in django-tables2 docs: <a href="https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/>https://django-tables2.readthedocs.io/en/latest/pages/template-tags.html#render-table" target="_blank">https://django-tables2.readthedocs.io/en/latest/pages/template-tags.html#render-table</a> <a href="#fnref7" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn8" class="footnote-item"><p><code>get-absolute-url</code> in Django docs: <a href="https://docs.djangoproject.com/en/3.1/ref/models/instances/#get-absolute-url" target="_blank">https://docs.djangoproject.com/en/3.1/ref/models/instances/#get-absolute-url</a> <a href="#fnref8" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn9" class="footnote-item"><p>Django - Creating forms from models: <a href="https://docs.djangoproject.com/en/3.1/topics/forms/modelforms/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/forms/modelforms/</a> <a href="#fnref9" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[TTL255 finalist of Cisco 2020 IT Blog Awards]]></title><description><![CDATA[Vote for TTL255.com in Cisco 2020 IT Blog Awards]]></description><link>https://ttl255.com/cisco-2020-it-blog-awards/</link><guid isPermaLink="false">5fdba64a1e69ff52c5b06639</guid><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Thu, 17 Dec 2020 18:45:42 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>I'm excited to announce that TTL255.com is one of the finalists in the <strong>Most Educational</strong> category of the 2020 IT Blog Awards, hosted by Cisco.</p>
<p>Over the years I learned great deal from blogs and videos created by community members. At one point I realized that I also might have something to offer and started this blog to give back to community hoping to teach and inspire others.</p>
<p>Creating valuable technical content takes a lot of work and time commitment. After years of posting here I appreciate even more all content makers out there that often don't ask for anything in return.</p>
<p>This year I decided to submit TTL255.com to 2020 IT Blog Awards hoping to reach more people and see where that takes me.</p>
<p>If you find my content valuable and worth your time, please consider voting for TTL255.com by following the below link:</p>
<p><a href="https://www.ciscofeedback.vovici.com/se/705E3ECD2A8D7180">https://www.ciscofeedback.vovici.com/se/705E3ECD2A8D7180</a></p>
<p><img src="https://ttl255.com/content/images/2020/12/ITBlogAwards_2020_Badge-Finalist-MostEducational.png" alt="ITBlogAwards_2020_Badge-Finalist-MostEducational"></p>
<p>You can find me in the <strong>Most Educational</strong> category:</p>
<p><img src="https://ttl255.com/content/images/2020/12/blog-awards-entry2.png" alt="blog-awards-entry"></p>
<p>While you there have a look at other amazing blog posts. Some of them might inspire you, some will teach you something new. All come from members of community that put themselves out there to share their knowledge with all of us.</p>
<p>Last but not least, remember that someone will always find what you have to say interesting, so if you don't have one already, consider starting your own blog!</p>
<p>Thanks for reading.</p>
<p>Przemek</p>
</div>]]></content:encoded></item><item><title><![CDATA[Developing NetBox Plugin - Part 1 - Setup and initial build]]></title><description><![CDATA[Learn how to build your own NetBox plugins. First post deals with setting up development environment and building plugin base.]]></description><link>https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/</link><guid isPermaLink="false">5fd333851e69ff52c5b0662f</guid><category><![CDATA[netbox]]></category><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[tools]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Fri, 11 Dec 2020 12:16:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>This is first post in my series showing how to develop NetBox plugin. We'll talk about what NetBox plugins are and why would you want one. Then I'll show you how to set up development environment. We'll finish by building base version of our custom plugin.</p>
<h2 id="developingnetboxplugintutorialseries">Developing NetBox Plugin tutorial series</h2>
<ul>
<li>Developing NetBox Plugin - Part 1 - Setup and initial build</li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/">Developing NetBox Plugin - Part 2 - Adding web UI pages</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-3-adding-search/">Developing NetBox Plugin - Part 3 - Adding search panel</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/">Developing NetBox Plugin - Part 4 - Small improvements</a></li>
<li><a href="https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/">Developing NetBox Plugin - Part 5 - Permissions and API</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#what-are-netbox">What are NetBox plugins?</a></li>
<li><a href="#why-plugins">Why plugins?</a></li>
<li><a href="#development-envi">Development environment set-up</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#setting-up-appli">Setting up application package</a></li>
</ul>
</li>
<li><a href="#our-plugin-bgp-p">Our plugin - BGP Peering</a>
<ul>
<li><a href="#initializing-plu">Initializing plugin - PluginConfig</a></li>
<li><a href="#kick-off-the-ini">Kick off the initial build</a></li>
</ul>
</li>
<li><a href="#adding-menu-entr">Adding menu entry</a></li>
<li><a href="#data-model">Data model</a>
<ul>
<li><a href="#where-do-the-fie">Where do the field types come from?</a></li>
<li><a href="#adding-model-to">Adding model to admin panel</a></li>
</ul>
</li>
<li><a href="#model-migrations">Model Migrations </a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#resources">Resources</a></li>
<li><a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering" target="_blank">BGP Peering Plugin repository</a></li>
</ul>
<p><a name="what-are-netbox"></a></p>
<h2 id="whatarenetboxplugins">What are NetBox plugins?</h2>
<p>NetBox plugins are small, self-contained, applications that add new functionality. This could range from adding new API endpoint to fully fledged apps. These apps can provide their own data models, views, background tasks and more. We can also inject content into existing model pages. NetBox added plugin support in version 2.8.</p>
<p>Plugins can access existing objects and functionality of NetBox. This allows them to integrate with NetBox's look and feel. Apps can also use any libraries, external resources, and API calls thy want. One of restrictions is that we're not allowed to change existing NetBox's models. That would break the rule of plugins being self-contained reusable apps.</p>
<p>Under the hood plugins are Django apps. This means most of the resources on this topic available on the web can be used for creating NetBox plugins.</p>
<p>You can alredy find on the net plugins created by community. I included links to some in References.</p>
<p><a name="why-plugins"></a></p>
<h2 id="whyplugins">Why plugins?</h2>
<p>NetBox is a very focused project. This allowed it to provide high quality functionality without getting too bloated. Features provided by the core are what majority of user base needs and uses.</p>
<p>New features that are not widely used would take up time that maintainers have in short supply. Instead this time can be used on improving the core. Some requirements are also so specific that they wouldn't fit in the standard model.</p>
<p>For that reason NetBox's maintainers came up with the awesome idea of plugin system. Users can create self-contained plugins adding required functionality.</p>
<p>With plugins you can have your own data models, new APIs, etc. be part of NetBox with no need for custom fork. You can write your own app and iterate it at your pace. If you want, you can can share that app with community. It can then be installed in NetBox like you would install any Python package.</p>
<p>In other words, endless possibilities for building cool stuff!</p>
<p><a name="development-envi"></a></p>
<h2 id="developmentenvironmentsetup">Development environment set-up</h2>
<p>Before we start working on our plugin I'll show you my development setup. You can use your own setup if you have one, but you might find some inspiration here.</p>
<p><a name="prerequisites"></a></p>
<h3 id="prerequisites">Prerequisites</h3>
<p>I'm using NetBox 2.9+ with Python 3.8.5 under Ubuntu 20.04 and have following two Python utilities installed in userspace:</p>
<ul>
<li>poetry</li>
<li>invoke</li>
</ul>
<p>Poetry is used to manage dependencies and packaging of our app.</p>
<p>Invoke is a pure Python alternative to <code>make</code>. This allows us to define and execute commonly run tasks.</p>
<p>You will also need to have installed the Docker engine and the Docker Compose utility. These are used to run development environment in the container.</p>
<p><strong>Note</strong>: When installing Poetry on Ubuntu20.04 I had to install below package to force Poetry to user Python 3 during its install:</p>
<p><code>apt install python-is-python3</code></p>
<p>See more details here: <a href="https://wiki.ubuntu.com/FocalFossa/ReleaseNotes#Python3_by_default" target="_blank">https://wiki.ubuntu.com/FocalFossa/ReleaseNotes#Python3_by_default</a></p>
<p><a name="setting-up-appli"></a></p>
<h3 id="settingupapplicationpackage">Setting up application package</h3>
<p>With <code>poetry</code> and <code>invoke</code> in place we can start building scaffolding for our plugin.</p>
<ul>
<li>Create, and change into, directory where you'll keep plugin:</li>
</ul>
<pre><code class="language-text">$ mkdir ttl255-netbox-plugin-bgppeering &amp;&amp; cd ttl255-netbox-plugin-bgppeering
</code></pre>
<ul>
<li>Activate Python virtual environment with <code>poetry</code>:</li>
</ul>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ poetry shell
</code></pre>
<p>Your prompt should change to let you know you're inside of Python Venv, see example below:</p>
<pre><code class="language-text">(ttl255-netbox-plugin-bgppeering-6_wYw8eP-py3.8) \
        przemek@quark:~/netdev/ttl255-netbox-plugin-bgppeering$ 
</code></pre>
<ul>
<li>Inside of plugin directory ask <code>poetry</code> to initialize your package with <code>poetry init</code>:</li>
</ul>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ poetry init

This command will guide you through creating your pyproject.toml config.

Package name [ttl255-netbox-plugin-bgppeering]:  
Version [0.1.0]:  
Description []:  NetBox Plugins - adds BGP Peering model
Author [None, n to skip]:  Przemek Rogala (ttl255.com)
License []:  Apache-2.0
Compatible Python versions [^3.8]:  

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file

[tool.poetry]
name = &quot;ttl255-netbox-plugin-bgppeering&quot;
version = &quot;0.1.0&quot;
description = &quot;NetBox Plugins - adds BGP Peering model&quot;
authors = [&quot;Przemek Rogala (ttl255.com)&quot;]
license = &quot;Apache-2.0&quot;

[tool.poetry.dependencies]
python = &quot;^3.8&quot;

[tool.poetry.dev-dependencies]

[build-system]
requires = [&quot;poetry-core&gt;=1.0.0&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;


Do you confirm generation? (yes/no) [yes] yes
</code></pre>
<p>As you can see above you will be asked a few questions about your package. You need to provide name, version, description, etc. Once you're happy with everything <code>poetry</code> will generate <code>pyproject.toml</code> file with details of your package.</p>
<p>I used long name for my package to give it namespace and descriptive name in case I wanted to push it out to PyPi. It's unlikely that anyone else would use this name for the package.</p>
<p>When plugin is added to NetBox I want to use shorter name. I'm going to show you how to do it.</p>
<ul>
<li>Create directory with name that will be used when importing package:</li>
</ul>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ mkdir netbox_bgppeering
</code></pre>
<p>I want my plugin to be called <code>netbox_bgppeering</code> when it's added in NetBox. Directory we just created will store source code of our plugin.</p>
<ul>
<li>Tell <code>poetry</code> to include package in <code>netbox_bgppeering</code> directory:</li>
</ul>
<p>We add the below to the <code>[tool.poetry</code>] config section in <code>pyproject.toml</code>:</p>
<pre><code>packages = [
    { include = &quot;netbox_bgppeering&quot; },
]
</code></pre>
<p>Now when our plugin is imported it can be referred by <code>netbox_bgppeering</code> package name.</p>
<ul>
<li>Next we tell <code>poetry</code> to add Python libraries that are used during development:</li>
</ul>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ poetry add bandit black invoke \
        pylint pylint-django pydocstyle yamllint --dev
</code></pre>
<p>All these packages will be added to dev dependencies in <code>pyproject.toml</code>. <code>Poetry</code> will also create new file <code>poetry.lock</code>. In there your dependencies are described in details and locked to specific versions. This allows you and other developers to recreate your Python environment.</p>
<ul>
<li>Add <code>invoke</code> tasks and environment build files:</li>
</ul>
<p>Now we can take advantage of great work done by Network To Code folks. We'll borrow some files from NetBox Onboarding plugin. These will greatly help with standing up and managing development environment.</p>
<p>Navigate to or clone the repo <a href="https://github.com/networktocode/ntc-netbox-plugin-onboarding" target="_blank">https://github.com/networktocode/ntc-netbox-plugin-onboarding</a></p>
<p><strong>Note</strong>: Repository is under Apache-2.0 license. Don't forget to keep relevant copyright information like license headers etc. if you intend to release work based on it.</p>
<p>Below are the files that I copied over, and use, in my workspace:</p>
<pre><code class="language-text">tasks.py
development/*
</code></pre>
<ul>
<li>Update plugin and Docker image names in <code>tasks.py</code> and <code>development/*</code> files:</li>
</ul>
<p>You will need to review and update names of the plugin, Docker image, etc. in the following files. These should match the name of your plugin.</p>
<ul>
<li><code>tasks.py</code></li>
<li><code>development/docker-compose.yml</code></li>
</ul>
<p>My repo has these files if you want to see how I've done it.</p>
<ul>
<li>Add your plugin to NetBox configuration file:</li>
</ul>
<p>Find the <code>PLUGINS</code> and <code>PLUGINS_CONFIG</code> settings in <code>development/base_configuration.py</code> and add your plugin there.</p>
<p>You use first setting to enable your plugin. Second one is used to pass configuration settings expected by your plugin.</p>
<p>I don't currently have any setting so I'm using empty values here.</p>
<pre><code>PLUGINS = [&quot;netbox_bgppeering&quot;]

PLUGINS_CONFIG = {&quot;netbox_bgppeering&quot;: {}}
</code></pre>
<p>With all that in place we can start building our plugin.</p>
<p><a name="our-plugin-bgp-p"></a></p>
<h2 id="ourpluginbgppeering">Our plugin - BGP Peering</h2>
<p>For this blog series I'm building plugin that can record details of BGP peers.</p>
<p>The idea is to be able to record and track information on BGP peer connections. I want to be able to keep the below details on each of the peers I have a BGP sessions with:</p>
<ul>
<li>Site (DC, etc.) where this peer connects</li>
<li>Device on which peering takes place</li>
<li>Local IP which we use for peering</li>
<li>Local AS number we use for peering</li>
<li>Remote IP that our peer uses</li>
<li>Remote AS that peer uses</li>
<li>Peer name</li>
<li>Description to add more context</li>
</ul>
<p>Some of that information that doesn't fit into NetBox's standard model. This is a perfect use case for writing plugin and custom models.</p>
<p><a name="initializing-plu"></a></p>
<h3 id="initializingpluginpluginconfig">Initializing plugin - PluginConfig</h3>
<p>First thing we need to do when writing NetBox plugin is to create plugin config. This goes into <code>__init__.py</code> file in plugin's directory. Most of the plugins will inherit from <code>PluginConfig</code> class, unless they have some special requirements.</p>
<p>In my case I created class <code>BgpPeering</code> that subclasses <code>PluginConfig</code>:</p>
<p><code>__init__.py</code></p>
<pre><code>from extras.plugins import PluginConfig


class BgpPeering(PluginConfig):
    name = &quot;bgp_peering&quot;
    verbose_name = &quot;BGP Peering&quot;
    description = &quot;Manages BGP peer connections&quot;
    version = &quot;0.1&quot;
    author = &quot;Przemek Rogala (ttl255.com)&quot;
    author_email = &quot;pr@ttl255.com&quot;
    base_url = &quot;bgp-peering&quot;
    required_settings = []
    default_settings = {}


config = BgpPeering
</code></pre>
<p>This class has a number of attributes that describe our plugin. The important ones are:</p>
<ul>
<li><code>name</code> - this is the name of your plugin and it has to match the name of your package as defined in <code>poetry.toml</code> file.</li>
<li><code>verbose_name</code> - human friendly name of the plugin.</li>
<li><code>description</code> - short description of what our plugin does.</li>
<li><code>base-url</code> - this defines base URL for our plugin that is appended to <code>/plugin/</code> NetBox URL.</li>
<li><code>required_settings</code> - this a list of settings that must be defined by user of the plugin.</li>
<li><code>default_settings</code> - here you include dictionary with plugin settings and their default values.</li>
</ul>
<p>I have no settings at the moment but wanted to include attributes already in case I find need for them later.</p>
<p><strong>Note</strong>: I'm only using a subset of attributes. For full list of attributes refer to official docs <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></p>
<p>If you want to follow along I created git branch with all the code we created up until this point: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/initial-plugin" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/initial-plugin</a></p>
<p><a name="kick-off-the-ini"></a></p>
<h3 id="kickofftheinitialbuild">Kick off the initial build</h3>
<p>With plugin config in place we can run initial build of our NetBox dev environment using <code>invoke build</code> command.</p>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ invoke build

... (cut fo brevity)

Successfully built 228ef14eb72b
Successfully tagged ttl255-netbox-plugin-bgppeering/netbox:master-py3.8
</code></pre>
<p>Once the command finished executing you should have a set of containers that will allow us to spin up NetBox for testing and iterating our plugin.</p>
<p>With build in place we should bring NetBox up for the first time. We can either use <code>invoke start</code> to run it in the background or <code>invoke debug</code> and see all console messages in our shell.</p>
<p>I'm going to run <code>invoke start</code>.</p>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ invoke start
Starting Netbox in detached mode.. 
Creating network &quot;netbox_bgppeering_default&quot; with the default driver
Creating volume &quot;netbox_bgppeering_pgdata_netbox_bgppeering&quot; with default driver
Creating netbox_bgppeering_redis_1    ... done
Creating netbox_bgppeering_postgres_1 ... done
Creating netbox_bgppeering_netbox_1   ... done
Creating netbox_bgppeering_worker_1   ... done
</code></pre>
<p>You can check with <code>docker ps</code> if all containers are running:</p>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ docker ps
CONTAINER ID        IMAGE                                                 COMMAND                  CREATED              STATUS                   PORTS                                                                         NAMES
0a97b5d04b86        ttl255-netbox-plugin-bgppeering/netbox:master-py3.8   &quot;sh -c 'python manag…&quot;   28 seconds ago       Up 27 seconds                                                                                          netbox_bgppeering_worker_1
07075c544b52        ttl255-netbox-plugin-bgppeering/netbox:master-py3.8   &quot;sh -c 'python manag…&quot;   28 seconds ago       Up 27 seconds            0.0.0.0:8000-&gt;8000/tcp                                                        netbox_bgppeering_netbox_1
7096a4a8a643        postgres:10                                           &quot;docker-entrypoint.s…&quot;   About a minute ago   Up 28 seconds            5432/tcp                                                                      netbox_bgppeering_postgres_1
1ce3bf4834b4        redis:5-alpine                                        &quot;docker-entrypoint.s…&quot;   About a minute ago   Up 28 seconds            6379/tcp                                                                      netbox_bgppeering_redis_1
</code></pre>
<p>With all that in place we should now create superuser account. This will allow us access to admin panel.</p>
<pre><code class="language-text">..ttl255-netbox-plugin-bgppeering$ invoke create-user --user ttl255

Email address: 
Password: 
Password (again): 
Superuser created successfully.
</code></pre>
<p>And now time for the big moment. If everything worked as it should you can navigate to <a href="http://localhost:8000/" target="_blank">http://localhost:8000/</a> and login with you superuser credentials.</p>
<p>Once you're logged in you might wonder if our plugin made it to Netbox. We can't see anything extra, as we didn't really create anything substantial.</p>
<p>But don't worry, there's a way to check it if plugin is there.</p>
<p>Navigate to <code>admin &gt; System - Installed plugins</code>.</p>
<p>And tada, it's here!</p>
<p><img src="https://ttl255.com/content/images/2020/12/admin-plugin-installed.png" alt="admin-plugin-installed"></p>
<p>It's quite exciting. We just added some extra stuff to NetBox!</p>
<p>But as much fun as it is, it'd be nice if we could actually see this plugin in action.</p>
<p><a name="adding-menu-entr"></a></p>
<h2 id="addingmenuentry">Adding menu entry</h2>
<p>I'm going to add a menu entry for our plugin. This will prove that we can add new elements to GUI and it'll give us something tangible.</p>
<ul>
<li>First we'll create <code>url.py</code> which Django, and NetBox, use to map URLs used by our plugin to code that generates content for these URLs.</li>
</ul>
<p><code>urls.py</code></p>
<pre><code>from django.&lt;a href=&quot;http&quot; target=&quot;_blank&quot;&gt;http&lt;/a&gt; import HttpResponse
from django.urls import path


def dummy_view(request):
    html = &quot;&lt;html&gt;&lt;body&gt;BGP Peering plugin.&lt;/body&gt;&lt;/html&gt;&quot;
    return HttpResponse(html)


urlpatterns = [
    path(&quot;&quot;, dummy_view, name=&quot;bgppeering_list&quot;),
]
</code></pre>
<p>For now we have just one URL, an empty string. This is root URL of our plugin accessible at <code>&lt;netbox-url&gt;/plugins/bgp-peering/</code>.</p>
<p>I named link <code>bgppeering_list</code> to allow us to refer to this URL later by a convenient name instead of hardcoding it.</p>
<p>I also created temporary function <code>dummy_view</code> returning dummy content. This will allow us to test the link.</p>
<ul>
<li>Next we'll create file <code>navigation.py</code> where menu elements used by our plugin have to go.</li>
</ul>
<p><code>navigation.py</code></p>
<pre><code>from extras.plugins import PluginMenuItem


menu_items = (
    PluginMenuItem(
        link=&quot;plugins:netbox_bgppeering:bgppeering_list&quot;,
        link_text=&quot;BGP Peerings&quot;,
    ),
)
</code></pre>
<p>Here we are adding single element to our plugin's menu. We define display name in <code>link_text</code> variable and <code>link</code> variable points to URL we defined in <code>urls.py</code>. You can see here that we used previously defined name <code>bgppeering_list</code>.</p>
<p>Class we imported here, <code>PluginMenuItem</code>, comes from NetBox.</p>
<p>The link name is automatically put in the namespace <code>plugins:&lt;plugin_name&gt;</code> where <code>&lt;plugin_name&gt;</code> is the name we defined in <code>PluginConfig</code> in <code>__init__.py</code>. This is why the final URL name we used is <code>plugins:netbox_bgppeering:bgppeering_list</code>.</p>
<p>With these menu item and url in place we can rebuild the image and bring it up for testing.</p>
<pre><code>...ttl255-netbox-plugin-bgppeering$ invoke stop
...ttl255-netbox-plugin-bgppeering$ invoke build
...ttl255-netbox-plugin-bgppeering$ invoke start
</code></pre>
<p>After few seconds NetBox should be up again. Navigate to it and check the top menu bar.</p>
<p>Look at that! Our plugin shows in the <code>plugins</code> top menu with the menu item we defined.</p>
<p><img src="https://ttl255.com/content/images/2020/12/minimal-plugin.png" alt="minimal-plugin"></p>
<p>When we click on the menu item we will get text only response from our dummy function.</p>
<p><img src="https://ttl255.com/content/images/2020/12/min-plugin-view.png" alt="min-plugin-view"></p>
<p>That's pretty cool. We have a plugin that shows up in NetBox and can actually do something!</p>
<p>All the code up to this point is in branch <code>minimal-plugin</code> if you want to check it out: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/minimal-plugin" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/minimal-plugin</a></p>
<p><a name="data-model"></a></p>
<h2 id="datamodel">Data model</h2>
<p>To give our plugin some substance we'll now work on the data model for our plugin.</p>
<p>In the world of Django model is a set of fields and behaviour of the data we want to store. Each model maps to an underlying database table.</p>
<p>As a reminder, these are the attributes I want to have in my data model:</p>
<ul>
<li>site - peer connects here</li>
<li>device - peering takes place here</li>
<li>local_ip - IP address we use for peering</li>
<li>local_as - BGP ASN number we use for peering</li>
<li>remote_ip - IP address our peer uses</li>
<li>remote_as - BGP ASN number our peer uses</li>
<li>peer_name - what our peer is called</li>
<li>description - adds more context about this connection</li>
</ul>
<p>Looking at the details I want to record, I can see that we can link some of those attributes into NetBox's model. For instance device is something that NetBox already has a model for. Same for local IP, which we'd expect to be assigned to an interface on the device where peering takes place.</p>
<p>What about remote IP? We could link it but I'm not going to at this stage. I don't know yet if I want to force these IPs to be in NetBox. We could always change it later.</p>
<p>Right, so how do we go about creating a model?</p>
<p>In vanilla Django models are classes that sublcass <code>django.db.models.Model</code>. In our case we'll take advantage of NetBox class <code>extras.models.ChangeLoggedModel</code>. This will automatically enable change logging for instances of model.</p>
<p>In Django database models need to be in <code>models.py</code> file, so we'll create that file and record our model there.</p>
<p><code>model.py</code></p>
<pre><code>from django.db import models

from dcim.fields import ASNField
from extras.models import ChangeLoggedModel
from ipam.fields import IPAddressField


class BgpPeering(ChangeLoggedModel):
    site = models.ForeignKey(
        to=&quot;dcim.Site&quot;, on_delete=models.SET_NULL, blank=True, null=True
    )
    device = models.ForeignKey(to=&quot;dcim.Device&quot;, on_delete=models.PROTECT)
    local_ip = models.ForeignKey(to=&quot;ipam.IPAddress&quot;, on_delete=models.PROTECT)
    local_as = ASNField(help_text=&quot;32-bit ASN used locally&quot;)
    remote_ip = IPAddressField(help_text=&quot;IPv4 or IPv6 address (with mask)&quot;)
    remote_as = ASNField(help_text=&quot;32-bit ASN used by peer&quot;)
    peer_name = models.CharField(max_length=64, blank=True)
    description = models.CharField(max_length=200, blank=True)
</code></pre>
<p>There are a few things to unpack here, so let's run through this in more detail.</p>
<p>First we have a few import statements.</p>
<ul>
<li><code>from django.db import models</code> - this is Django module providing us with standard field types like <code>CharField</code> or <code>ForeignKey</code>.</li>
<li><code>from extras.models import ChangeLoggedModel</code> - Here we borrow model class defined in NetBox, <code>ChangeLoggedModel</code>.</li>
<li><code>from dcim.fields import ASNField</code> - NetBox defines <code>ASNField</code> class that we can use for our BGP AS attributes. This supports both 16 and 32 bit ASNs.</li>
<li><code>from ipam.fields import IPAddressField</code> - Another class provided by NetBox, this one handles IPv4/IPv6 addresses for us.</li>
</ul>
<p>Next step is creating model class. Here I created class named <code>BgpPeering</code> which subclasses <code>ChangeLoggedModel</code>.</p>
<pre><code>class BgpPeering(ChangeLoggedModel):
</code></pre>
<p>Next we have a number of model fields.</p>
<ul>
<li>
<p><code>site</code></p>
<pre><code>    site = models.ForeignKey(
        to=&quot;dcim.Site&quot;, on_delete=models.SET_NULL, blank=True, null=True
    )
</code></pre>
<p>Field <code>site</code> links to NetBox's <code>dcim.Site</code> model so I made it a <code>ForeignKey</code>.</p>
<ul>
<li><code>null=True</code> allows the corresponding database column to be NULL (contain no value).</li>
<li><code>blank=True</code> means this field is optional when appearing in forms.</li>
</ul>
<p>The above two options basically mean that this field doesn't have to be filled and we can still record objects with it being empty.</p>
<ul>
<li><code>on_delete=models.SET_NULL</code> means that if NetBox site object to which we link is deleted we will set this field to NULL</li>
</ul>
</li>
<li>
<p><code>device</code></p>
<pre><code>    device = models.ForeignKey(
        to=&quot;dcim.Device&quot;,
        on_delete=models.PROTECT
    )
</code></pre>
<p>Field <code>device</code> is linked to <code>dcim.Device</code> model in NetBox. <code>null</code> and <code>blank</code> attributes are left to defaults meaning that this field is required and cannot be emtpy.</p>
<ul>
<li>on_delete=models.PROTECT - this means that if the linked device cannot be deleted as long as our object exists</li>
</ul>
</li>
<li>
<p><code>local_ip</code></p>
<pre><code>    local_ip = models.ForeignKey(
        to=&quot;dcim.Interface&quot;,
        on_delete=models.PROTECT
    )
</code></pre>
<p>This field is linked to <code>ipam.IPAddress</code> model. This field cannot be empty and the linked object can't be deleted.</p>
</li>
<li>
<p><code>local_as</code></p>
<pre><code>    local_as = ASNField(
        help_text=&quot;32-bit ASN used locally&quot;
    )
</code></pre>
<p>Next field, <code>local_as</code> is of <code>ASNField</code> type. This automatically enforces validity of ASNs. Help text is text that will provide additional context in forms where this field appears.</p>
</li>
<li>
<p><code>remote_ip</code></p>
<pre><code>    remote_ip = IPAddressField(
        help_text=&quot;IPv4 or IPv6 address (with mask)&quot;
    )
</code></pre>
<p>Field <code>remote_ip</code> is of <code>IPAddressField</code> type. This means only valid IPv4 and IPv6 will be allowed here. Help text is provided as well.</p>
</li>
<li>
<p><code>remote_as</code></p>
<pre><code>    remote_as = ASNField(
        help_text=&quot;32-bit ASN used by peer&quot;
    )
</code></pre>
<p>This field is same as <code>local_as</code> field, with slightly different help text.</p>
</li>
<li>
<p><code>peer_name</code> and <code>description</code></p>
<pre><code>    peer_name = models.CharField(
        max_length=64,
        blank=True
    )
    description = models.CharField(
        max_length=200,
        blank=True
    )
</code></pre>
<p>Finally <code>peer_name</code> and <code>description</code> fields store string, with max length of 64 and 200 characters respectively. We allow these fields to be empty.</p>
<p>This is a pretty basic model that we might have to add to in the future. But for now it will do.</p>
</li>
</ul>
<p><a name="where-do-the-fie"></a></p>
<h3 id="wheredothefieldtypescomefrom">Where do the field types come from?</h3>
<p>You might be wondering how did I know that <code>remote_ip</code> can use <code>IPAddressField</code> class. Or that <code>peer_name</code> being string can use <code>CharField</code>.</p>
<p>We can find all standard field classes in Django docs, <a href="https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-types" target="_blank">https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-types</a>, these are fields imported from &quot;django.db.models&quot;.</p>
<p>You can also find few custom fields classes in NetBox's source code, <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a> Best way to see how it all works is by investigating existing models. You'll learn a lot this way!</p>
<p>Do remember though that stuff you reuse from NetBox might change in the future. Most of the core is stable but keep that in mind.</p>
<p><a name="adding-model-to"></a></p>
<h3 id="addingmodeltoadminpanel">Adding model to admin panel</h3>
<p>We have defined our model but there is no way to interact with it yet. We'll work on forms and APIs in future posts but there's something we can do now.</p>
<p>With little effort we can add this model to admin panel. To do that we need to create file <code>admin.py</code> where admin related code goes.</p>
<p><code>admin.py</code></p>
<pre><code>from django.contrib import admin
from .models import BgpPeering


@admin.register(BgpPeering)
class BgpPeeringAdmin(admin.ModelAdmin):
    list_display = (&quot;device&quot;, &quot;peer_name&quot;, &quot;remote_as&quot;, &quot;remote_ip&quot;)
</code></pre>
<p>We create <code>BgpPeeringAdmin</code> class which subclasses <code>ModelAdmin</code> class. Then we use <code>admin.register</code> decorator to register our <code>BgpPeering</code> model with it.</p>
<p>As a result our model will be accessible in admin panel and we'll be able to interact with it.</p>
<p>But before that happens there's something very important we need to do. We need to create migrations for our model.</p>
<p>Source code up until point is in branch <code>adding-model</code>: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/adding-model" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/adding-model</a></p>
<p><a name="model-migrations"></a></p>
<h2 id="modelmigrations">Model Migrations</h2>
<p>We now defined our model in the code but that needs to be added to database before our plugin can use it.</p>
<p>Migrations are what Django uses to propagate changes we make to our models into database schema. These are like change control for database schema. As our models grow and change migrations help Django keep track of them.</p>
<p>Django provides <code>makemigration</code> commands to help with generating migrations. In our environment we have wrapper for that which we can use by running <code>invoke makemigrations</code>.</p>
<p><strong>Note</strong>: I had trouble running <code>makemigrations</code> command on its own. I decided to extend <code>invoke.py</code> by adding extra argument <code>app_name</code> to it. By default this will use value stored in <code>BUILD_NAME</code> variable. By doing that I'm able to specify app for which Django will attempt to generate migrations.</p>
<p>So let's get to it. We'll rebuild our image first. Then we'll run <code>makemigrations</code> command:</p>
<pre><code class="language-text">...ttl255-netbox-plugin-bgppeering$ invoke build

... (cut for brevity)

...ttl255-netbox-plugin-bgppeering$ invoke makemigrations
netbox_bgppeering_postgres_1 is up-to-date
Starting netbox_bgppeering_postgres_1 ... done
Starting netbox_bgppeering_redis_1    ... done

Migrations for 'netbox_bgppeering':
  /source/netbox_bgppeering/migrations/0001_initial.py
    - Create model BgpPeering

... (cut for brevity)
</code></pre>
<p>Well, well. Something cool happened. Django created a directory with migration file containing instructions on how to add our model to database.</p>
<p><strong>Important</strong>: We need to distribute this with our plugin package as that's what Djano will use to update NetBox's database.</p>
<pre><code class="language-text">...ttl255-netbox-plugin-bgp-peering$ tree netbox_bgppeering/migrations/
netbox_bgppeering/migrations/
├── 0001_initial.py
└── __init__.py
</code></pre>
<p>Seems we have all pieces of the puzzle in place now. We'll rebuild the image and start environment in debug mode to see if our migrations is applied during startup.</p>
<pre><code class="language-text">...ttl255-netbox-plugin-bgppeering$ invoke stop

... (cut for brevity)

...ttl255-netbox-plugin-bgppeering$ invoke build

... (cut for brevity)

...ttl255-netbox-plugin-bgppeering$ invoke debug

... (cut for brevity)

netbox_1    | Running migrations:
netbox_1    |   Applying netbox_bgppeering.0001_initial... OK

... (cut for brevity)

</code></pre>
<p>There it is! Somewhere in the middle of the log messages we can see our migration being applied. Very cool!</p>
<p>Let's go into admin panel and see if it's there!</p>
<p><img src="https://ttl255.com/content/images/2020/12/admin-main-panel.png" alt="admin-main-panel"></p>
<p>Awesome, our model is showing up. Time to create some peering record.</p>
<p>Click on <code>Add</code> button and fill in the form that shows up.</p>
<p><img src="https://ttl255.com/content/images/2020/12/admin-peer-add-filled.png" alt="admin-peer-add-filled"></p>
<p>I already had <code>Site</code>, <code>Device</code> and <code>Local ip</code> created in Netbox to allow me to choose these in the drop down menus.</p>
<p>Now just click <code>save</code> and wait for result.</p>
<p><img src="https://ttl255.com/content/images/2020/12/admin-peer-created.png" alt="admin-peer-created"></p>
<p>And it's here. Our first BGP Peering connection record got created!</p>
<p>Source code up until this point is in branch <code>model-migrations</code>: <a href="https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/model-migrations" target="_blank">https://github.com/progala/ttl255-netbox-plugin-bgppeering/tree/model-migrations</a></p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>With that we come to the end of first post in the series on developing NetBox plugins. I hope that this post gave you a good idea of what NetBox plugins are and why you would want one.</p>
<p>You've seen how you can setup productive development environment when writing plugins. And you could also see what needs to be done to get your own plugin off the ground.</p>
<p>In the next post I'll show you how we can add templates and views that render them. This will allow us to work with our BGP Peering connections from the main GUI.</p>
<p>I hope you learned something today and I look forward to seeing you again!</p>
<p><a name="resources"></a></p>
<h2 id="resources">Resources</h2>
<ul>
<li>
<p>NetBox docs: Plugin Development: <a href="https://netbox.readthedocs.io/en/stable/plugins/development/" target="_blank">https://netbox.readthedocs.io/en/stable/plugins/development/</a></p>
</li>
<li>
<p>NetBox source code on GitHub: <a href="https://github.com/netbox-community/netbox" target="_blank">https://github.com/netbox-community/netbox</a></p>
</li>
<li>
<p>NetBox Extensibility Overview, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=FSoCzuWOAE0" target="_blank">https://www.youtube.com/watch?v=FSoCzuWOAE0</a></p>
</li>
<li>
<p>NetBox Plugins Development, NetBox Day 2020: <a href="https://www.youtube.com/watch?v=LUCUBPrTtJ4" target="_blank">https://www.youtube.com/watch?v=LUCUBPrTtJ4</a></p>
</li>
<li>
<p>NetBox Onboarding plugin: <a href="https://github.com/networktocode/ntc-netbox-plugin-onboarding" target="_blank">https://github.com/networktocode/ntc-netbox-plugin-onboarding</a></p>
</li>
<li>
<p>Netbox QR Code Plugin: <a href="https://github.com/k01ek/netbox-qrcode" target="_blank">https://github.com/k01ek/netbox-qrcode</a></p>
</li>
<li>
<p>NetBox Virtual Circuit Plugin: <a href="https://github.com/vapor-ware/netbox-virtual-circuit-plugin" target="_blank">https://github.com/vapor-ware/netbox-virtual-circuit-plugin</a></p>
</li>
<li>
<p>Dynamic DNS Connector for NetBox: <a href="https://github.com/sjm-steffann/netbox-ddns" target="_blank">https://github.com/sjm-steffann/netbox-ddns</a></p>
</li>
<li>
<p>Poetry docs: <a href="https://python-poetry.org/docs/" target="_blank">https://python-poetry.org/docs/</a></p>
</li>
<li>
<p>Invoke docs: <a href="http://docs.pyinvoke.org/en/stable/" target="_blank">http://docs.pyinvoke.org/en/stable/</a></p>
</li>
<li>
<p>Django models: <a href="https://docs.djangoproject.com/en/3.1/topics/db/models/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/db/models/</a></p>
</li>
<li>
<p>Django model field types: <a href="https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-types" target="_blank">https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-types</a></p>
</li>
<li>
<p>Django migrations: <a href="https://docs.djangoproject.com/en/3.1/topics/migrations/" target="_blank">https://docs.djangoproject.com/en/3.1/topics/migrations/</a></p>
</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Ansible - 'until' loop]]></title><description><![CDATA[This post discusses somewhat lesser known type of Ansible loop: "until" loop, which is used for retrying task until certain condition is met.]]></description><link>https://ttl255.com/ansible-until-loop/</link><guid isPermaLink="false">5fbabbde1e69ff52c5b06624</guid><category><![CDATA[Ansible]]></category><category><![CDATA[tools]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Mon, 23 Nov 2020 09:37:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="contents">Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#examples">Examples</a>
<ul>
<li><a href="#setup-details">Setup details</a></li>
<li><a href="#example-1-pollin">Example 1 - Polling web app status via API</a></li>
<li><a href="#example-2-wait-f">Example 2 - Wait for BGP to establish before retrieving peer routes</a></li>
<li><a href="#example-3-pollin">Example 3 - Polling health status of Docker container</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/ansible/until-loop" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="introduction"></a></p>
<h2 id="introduction">Introduction</h2>
<p>In this short post I'll introduce you to lesser known type of Ansible loop: &quot;until&quot; loop. This loop is used for retrying task until certain condition is met.</p>
<p>To use this loop in task you essentially need to add 3 arguments to your task arguments:</p>
<p><code>until</code> - condition that must be met for loop to stop. That is Ansible will continue executing the task until expression used here evaluates to true.<br>
<code>retry</code> - specifies how many times we want to run the task before Ansible gives up.<br>
<code>delay</code> - delay, in seconds, between retries.</p>
<p>As an example, below task will keep sending GET request to specified URL until the &quot;status&quot; key in response is equal to &quot;READY&quot;. We ask Ansible to make 10 attempts in total with delay of 1 second between each attempt. If after final attempt condition in <code>until</code> is still not met task is marked as failed.</p>
<pre><code>  - name: Wait until web app status is &quot;READY&quot;
    uri:
      url: &quot;{{ app_url }}/status&quot;
    register: app_status
    until: app_status.json.status == &quot;READY&quot;
    retries: 10
    delay: 1
</code></pre>
<p>What's so cool about this loop is that you can use it to actively check result of executing given task before proceeding to other tasks.</p>
<p>This is different to using <code>when</code> task argument for instance, where we only execute task <em>IF</em> condition is met. Here the condition <em>MUST</em> be met before we execute next task.</p>
<p>One is conditional execution, usually based on static check, i.e. existence of package or feature, or value of pre-defined variable. The other pauses execution until condition is met, and failing task if it isn't, to ensure desired state is in place before proceeding.</p>
<p>Some scenarios where <code>until</code> loop could be useful:</p>
<ul>
<li>Making sure web app service came up before progressing Playbook.</li>
<li>Checking status via API endpoint of long running asynchronous task.</li>
<li>Waiting for routing protocol adjacency to come up.</li>
<li>Waiting for convergence of the system, e.g. routing in networking.</li>
<li>Checking if Docker container is reporting as healthy.</li>
<li>Retrying service that might take multiple attempts to come up fully.</li>
</ul>
<p>Basically, there are a lot of use cases for <code>until</code> loop :)</p>
<p>Note that some of the above can also be achieved with <code>wait_for</code> module, which is a bit more specialized. Module <code>wait_for</code> can check status of ports, files and processes, among other things. Have a look at link in <a href="#references">References</a> if you want to find out more.</p>
<p><a name="examples"></a></p>
<h2 id="examples">Examples</h2>
<p>We now know what <code>until</code> loop is, how to use it, and where it could be useful. Next we'll now through some examples to give you a better intuition of how one would go about using it in Playbooks.</p>
<p><a name="setup-details"></a></p>
<h3 id="setupdetails">Setup details</h3>
<p>Details of the setup used for the examples:</p>
<ul>
<li>Python 3.8.5</li>
<li>Ansible 2.9.10 running in Python virtual environment</li>
<li>Python libraries listed in <code>requirements.txt</code> in the GitHub repository for this post</li>
<li>Docker engine</li>
<li>Docker container named &quot;veos:4.18.10M&quot; built with vrnetlab and &quot;vEOS-lab-4.18.10M.vmdk&quot; image</li>
</ul>
<p><a name="example-1-pollin"></a></p>
<h3 id="example1pollingwebappstatusviaapi">Example 1 - Polling web app status via API</h3>
<p>In first example I have a Playbook that gets content of home page of a web app. Twist is that this web app takes some time to fully come up. Fortunately there is an API endpoint that we can query to check if the app is ready to accept requests.</p>
<p>We'll take advantage of the <code>until</code> loop to keep polling the status until we get green light to proceed.</p>
<p><code>until_web_app.yml</code></p>
<pre><code>---
- name: &quot;PLAY 1. Use 'until' to wait for Web APP to come up.&quot;
  hosts: local
  connection: local
  gather_facts: no

  vars:
    app_url: &quot;http://127.0.0.1:5010&quot;

  tasks:
  - name: &quot;TASK 1.1. Start Web app (async 20 keeps up app in bg for 20 secs).&quot;
    command: python flask_app/main.py
    async: 20
    poll: 0
    changed_when: no

  - name: &quot;TASK 1.2. Retrieve Web app home page (should fail).&quot;
    uri:
      url: &quot;{{ app_url }}&quot;
    register: app_hp
    ignore_errors: yes

  - name: &quot;TASK 1.3. Display HTTP code returned by home page.&quot;
    debug:
      msg: &quot;Web app returned {{ app_hp.status }} HTTP code&quot;

  - name: &quot;TASK 1.4. Wait until GET to 'status' returns 'READY'.&quot;
    uri:
      url: &quot;{{ app_url }}/status&quot;
    register: app_status
    until: app_status.json.status == &quot;READY&quot;
    retries: 10
    delay: 1
    
  - name: &quot;TASK 1.5. Retrieve Web app home page (should succeed now).&quot;
    uri:
      url: &quot;{{ app_url }}&quot;
    register: app_hp

  - name: &quot;TASK 1.6. Display HTTP code and body returned by home page.&quot;
    debug:
      msg: 
        - &quot;Web app returned {{ app_hp.status }} HTTP code&quot;
        - &quot;Web page content: {{ lookup('url', app_url) }}&quot;
</code></pre>
<p>Let's have a look at interesting bits in this Playbook.</p>
<p>I built this app with API endpoint that returns status of the service in the json payload. This can be either &quot;NOT_READY&quot; or &quot;READY&quot;.</p>
<ul>
<li>
<p>In TASK 1.1 we launch a small Flask Web App that takes 10 seconds to fully come up. I use <code>async</code> argument here to trick Ansible into keeping this up in background for 20 seconds, otherwise the Playbook would get stuck on this task.</p>
</li>
<li>
<p>In TASK 1.2 we get an error while retrieving home page because App is not ready yet.</p>
</li>
<li>
<p>In TASK 1.4 we use <code>until</code> loop to keep querying the <code>status</code> endpoint until returned value equals &quot;READY&quot;. Only when the task succeeds will we proceed to the next task where we again retrieve home page, now knowing that our chance of succeeding is much higher.</p>
</li>
<li>
<p>In TASK 1.5 we retrieve home page again, which should now succeed, contents of which we'll display in TASK 1.6.</p>
</li>
</ul>
<p>A lot of different Web API services expose some kind of <code>status</code> or <code>healthcheck</code> endpoint so this example shows a very useful pattern that we can use elsewhere.</p>
<p>If you're curiouse, you can find code of the Flask app in the Github repository together with the playbook.</p>
<p>And this is the output from the Playbook run:</p>
<pre><code class="language-text">venv) przemek@quark:~/netdev/repos/ans_unt$ ansible-playbook -i hosts.yml until_web_app.yml 

PLAY [PLAY 1. Use 'until' to wait for Web APP to come up.] *********************************************************************************************************************************************

TASK [TASK 1.1. Start Web app (async 20 keeps up app in bg for 20 secs).] ******************************************************************************************************************************
ok: [localhost]

TASK [TASK 1.2. Retrieve Web app home page (should fail).] *********************************************************************************************************************************************
fatal: [localhost]: FAILED! =&gt; {&quot;changed&quot;: false, &quot;content&quot;: &quot;&quot;, &quot;content_length&quot;: &quot;0&quot;, &quot;content_type&quot;: &quot;text/html; charset=utf-8&quot;, &quot;date&quot;: &quot;Sun, 22 Nov 2020 16:47:37 GMT&quot;, &quot;elapsed&quot;: 0, &quot;msg&quot;: &quot;Status code was 503 and not [200]: HTTP Error 503: SERVICE UNAVAILABLE&quot;, &quot;redirected&quot;: false, &quot;server&quot;: &quot;Werkzeug/1.0.1 Python/3.8.5&quot;, &quot;status&quot;: 503, &quot;url&quot;: &quot;http://127.0.0.1:5010&quot;}
...ignoring

TASK [TASK 1.3. Display HTTP code returned by home page.] **********************************************************************************************************************************************
ok: [localhost] =&gt; {
    &quot;msg&quot;: &quot;Web app returned 503 HTTP code&quot;
}

TASK [TASK 1.4. Wait until GET to 'status' returns 'READY'.] *******************************************************************************************************************************************
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (10 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (9 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (8 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (7 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (6 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (5 retries left).
FAILED - RETRYING: TASK 1.4. Wait until GET to 'status' returns 'READY'. (4 retries left).
ok: [localhost]

TASK [TASK 1.5. Retrieve Web app home page (should succeed now).] **************************************************************************************************************************************
ok: [localhost]

TASK [TASK 1.6. Display HTTP code and body returned by home page.] *************************************************************************************************************************************
ok: [localhost] =&gt; {
    &quot;msg&quot;: [
        &quot;Web app returned 200 HTTP code&quot;,
        &quot;Web page content: Service ready for use.&quot;
    ]
}

PLAY RECAP *********************************************************************************************************************************************************************************************
localhost                  : ok=6    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   
</code></pre>
<p><a name="example-2-wait-f"></a></p>
<h3 id="example2waitforbgptoestablishbeforeretrievingpeerroutes">Example 2 - Wait for BGP to establish before retrieving peer routes</h3>
<p>In the world of networking we often encounter situations where some kind of adjacency, be it BFD, PIM or BGP has to be established before we can retrieve information that is of interest.</p>
<p>To illustrate this I wrote a Playbook that waits for BGP peering to come up before checking routes we receive from neighbors.</p>
<p>Bear in mind this is simplified for use in an example, in the real world you might need to add more checks to ensure routing information between peer has been fully exchanged.</p>
<p><code>until_eos_net.yml</code></p>
<pre><code>---
- name: &quot;PLAY 1. Use 'until' to wait for BGP sessions to establish.&quot;
  hosts: veos_net
  gather_facts: no

  tasks: 
  - name: &quot;TASK 1.1. Record peer IPs for use in 'until' task.&quot;
    eos_command:
      commands: 
      - command: show ip bgp summary
        output: json
    register: init_bgp_sum

  - name: &quot;TASK 1.2. Forcefully reset BGP sessions.&quot;
    eos_command:
      commands: clear ip bgp neighbor *

  - name: &quot;TASK 1.3. Use 'until' to wait for all BGP sessions to establish.&quot;
    eos_command:
      commands:
      - command: show ip bgp summary
        output: json
    register: u_bgp_sum
    until: u_bgp_sum.stdout.0.vrfs.default.peers[item.key].peerState == &quot;Established&quot;
    retries: 15
    delay: 1
    loop: &quot;{{ init_bgp_sum.stdout.0.vrfs.default.peers | dict2items }}&quot;
    loop_control:
      label: &quot;{{ item.key }}&quot;

  - name: &quot;TASK 1.4. Retrieve neighbor routes.&quot;
    eos_command:
      commands:
      - command: &quot;show ip bgp neighbors {{ item.key }} routes&quot;
        output: json
    register: nbr_routes
    loop: &quot;{{ init_bgp_sum.stdout.0.vrfs.default.peers | dict2items }}&quot;
    loop_control:
      label: &quot;{{ item.key }}&quot;

  - name: &quot;TASK 1.5. Display neighbor routes.&quot;
    debug:
      msg: 
        - &quot;{{ ''.center(80, '=') }}&quot;
        - &quot;Neighbor: {{ nbr.item.key }}&quot;
        - &quot;{{ nbr.stdout.0.vrfs.default.bgpRouteEntries.keys() | list }}&quot;
        - &quot;{{ ''.center(80, '=') }}&quot;
    loop: &quot;{{ nbr_routes.results }}&quot;
    loop_control:
      loop_var: nbr
      label: &quot;{{ nbr.item.key }}&quot;

</code></pre>
<p>Again, we'll look more closely at tasks that do something interesting.</p>
<ul>
<li>
<p>In TASK 1.1 we record output of <code>show ip bgp summary</code> that we'll be used to iterate over list of BGP neighbors.</p>
</li>
<li>
<p>In TASK 1.3 we have core of our logic.</p>
<ul>
<li>Using <code>until</code> loop we keep checking status of each of the peers until all of them report &quot;Established&quot; value.</li>
<li>During each retry output of <code>show ip bgp summary</code> is recorded in <code>u_bgp_sum</code> variable.</li>
<li>To add to fun, <code>until</code> loop is run inside of a standard outer loop. Outer loop feeds <code>until</code> IPs of the peers so that it's easier to access data structure recorded in <code>u_bgp_sum</code>.</li>
</ul>
</li>
<li>
<p>In TASK 1.4 we can get routes received from each neighbor knowing that all of the peerings are now established. These routes are displayed in TASK 1.5.</p>
</li>
</ul>
<p>Waiting for convergence, or adjacency to get up, is another use case that comes up often. Hopefully this example illustrates how we can handle these.</p>
<p>You can also see here that <code>until</code> loop happily cooperates with standard loop allowing us to handle even more use cases.</p>
<p>Below is the result of running this Playbook.</p>
<pre><code class="language-text">(venv) przemek@quark:~/netdev/repos/ans_unt$ ansible-playbook -i hosts.yml until_eos_net.yml 

PLAY [PLAY 1. Use 'until' to wait for BGP sessions to establish.] **************************************************************************************************************************************

TASK [TASK 1.1. Record peer IPs for use in 'until' task.] **********************************************************************************************************************************************
ok: [veos01]

TASK [TASK 1.2. Forcefully reset BGP sessions.] ********************************************************************************************************************************************************
ok: [veos01]

TASK [TASK 1.3. Use 'until' to wait for all BGP sessions to establish.] ********************************************************************************************************************************
FAILED - RETRYING: TASK 1.3. Use 'until' to wait for all BGP sessions to establish. (15 retries left).
FAILED - RETRYING: TASK 1.3. Use 'until' to wait for all BGP sessions to establish. (14 retries left).
FAILED - RETRYING: TASK 1.3. Use 'until' to wait for all BGP sessions to establish. (13 retries left).
ok: [veos01] =&gt; (item=10.0.13.2)
ok: [veos01] =&gt; (item=10.0.12.2)
FAILED - RETRYING: TASK 1.3. Use 'until' to wait for all BGP sessions to establish. (15 retries left).
FAILED - RETRYING: TASK 1.3. Use 'until' to wait for all BGP sessions to establish. (14 retries left).
ok: [veos01] =&gt; (item=10.1.11.2)

TASK [TASK 1.4. Retrieve neighbor routes.] *************************************************************************************************************************************************************
ok: [veos01] =&gt; (item=10.0.13.2)
ok: [veos01] =&gt; (item=10.0.12.2)
ok: [veos01] =&gt; (item=10.1.11.2)

TASK [TASK 1.5. Display neighbor routes.] **************************************************************************************************************************************************************
ok: [veos01] =&gt; (item=10.0.13.2) =&gt; {
    &quot;msg&quot;: [
        &quot;================================================================================&quot;,
        &quot;Neighbor: 10.0.13.2&quot;,
        [
            &quot;192.168.0.0/25&quot;,
            &quot;192.168.1.0/25&quot;,
            &quot;192.168.4.0/24&quot;,
            &quot;192.168.7.0/24&quot;,
            &quot;192.168.6.0/24&quot;,
            &quot;10.50.255.3/32&quot;,
            &quot;192.168.5.0/24&quot;
        ],
        &quot;================================================================================&quot;
    ]
}
ok: [veos01] =&gt; (item=10.0.12.2) =&gt; {
    &quot;msg&quot;: [
        &quot;================================================================================&quot;,
        &quot;Neighbor: 10.0.12.2&quot;,
        [
            &quot;10.50.255.2/32&quot;,
            &quot;192.168.0.0/25&quot;
        ],
        &quot;================================================================================&quot;
    ]
}
ok: [veos01] =&gt; (item=10.1.11.2) =&gt; {
    &quot;msg&quot;: [
        &quot;================================================================================&quot;,
        &quot;Neighbor: 10.1.11.2&quot;,
        [],
        &quot;================================================================================&quot;
    ]
}

PLAY RECAP *********************************************************************************************************************************************************************************************
veos01                     : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
</code></pre>
<p><a name="example-3-pollin"></a></p>
<h3 id="example3pollinghealthstatusofdockercontainer">Example 3 - Polling health status of Docker container</h3>
<p>Docker is everywhere these days. There is a lot of tools like docker-compose that make running container easier. But we can also use Ansible to manage our containers.</p>
<p>Many containers these days come with built-in health checks which Docker engine can use to report on health of given container.</p>
<p>In this example I'll show you how we can talk to Docker to get the container status from inside of Ansible Playbook. Our goal is to launch 4 containers with virtual routers that we want to dynamically add to Ansible inventory. But we only want to do that once all of them came up fully.</p>
<p><code>until_docker.yml</code></p>
<pre><code>---
- name: &quot;PLAY 1. Provision lab with virtual routers.&quot;
  hosts: local
  connection: local
  gather_facts: no
    
  tasks:

  - name: &quot;TASK 1.1. Bring up virtual router containers.&quot;
    docker_container:
      name: &quot;{{ item }}&quot;
      image: &quot;{{ vr_image_name }}&quot;
      privileged: yes
    register: cont_data
    loop: &quot;{{ vnodes }}&quot;
    loop_control:
      pause: 10

  - name: &quot;TASK 1.2. Wait for virtual routers to finish booting.&quot;
    docker_container_info:
      name: &quot;{{ item }}&quot;
    register: cont_check
    until: cont_check.container.State.Health.Status == 'healthy'
    retries: 15
    delay: 25
    loop: &quot;{{ vnodes }}&quot;

  - name: &quot;TASK 1.3. Auto discover device IPs and add to inventory group.&quot;
    set_fact:
      dyn_grp: &quot;{{ dyn_grp | combine({cont_name: {'ansible_host': cont_ip_add }}) }}&quot;
    vars:
      cont_ip_add: &quot;{{ item.container.NetworkSettings.IPAddress }}&quot;
      cont_name: '{{ item.container.Name | replace(&quot;/&quot;, &quot;&quot;) }}'
      dyn_grp: {}
    loop: &quot;{{ cont_data.results }}&quot;
    loop_control:
      label: &quot;{{ cont_name }}&quot;

  - name: &quot;TASK 1.4. Dynamically create hosts.yml inventory.&quot;
    copy:
      content: &quot;{{ dyn_inv | to_nice_yaml }}&quot;
      dest: ./lab_hosts.yml
    vars:
      dyn_inv:
        &quot;{{ {'all': {'children': {inv_name: {'hosts': dyn_grp}}}} }}&quot;    
</code></pre>
<p>Of interest here are mostly TASK 1.1 and TASK 1.2. Remaining tasks deal with generating and saving inventory, but I wanted to leave them here to provide context.</p>
<p>Let's have a look at the first two tasks then.</p>
<ul>
<li>
<p>In TASK 1.1 we loop over container names recorded in <code>vnodes</code> var and we launch container for each of the entries. I added 10 second pause between launching each container to avoid  overwhelming my local Docker.</p>
</li>
<li>
<p>In TASK 1.2 we got our <code>until</code> loop inside of standard loop. In <code>until</code> loop we tell Docker to get info on container with name fed from outer loop. Then we check if value of health status is <code>healthy</code>. We'll keep retrying here until we get status we want, of if we exceed number of retries the task will fail.</p>
</li>
</ul>
<p>You might wonder how I chose the values for <code>retries</code> and <code>delay</code> arguments. These are completely arbitrary and depend on the machine and container that you're running. In my case I know from running these by hand that it takes some time for all containers to come up so 15 retries with 25 second delays fits my case well.</p>
<p>Now you can see that you can have Ansible poll status of your containers, pretty cool right?</p>
<p>To finish off, here's the result of this playbook being executed.</p>
<pre><code class="language-text">(venv) przemek@quark:~/netdev/repos/ans_unt$ ansible-playbook -i hosts.yml until_docker.yml 

PLAY [PLAY 1. Provision lab with virtual routers.] *****************************************************************************************************************************************************

TASK [TASK 1.1. Bring up virtual router containers.] ***************************************************************************************************************************************************
changed: [localhost] =&gt; (item=spine1)
changed: [localhost] =&gt; (item=spine2)
changed: [localhost] =&gt; (item=leaf1)
changed: [localhost] =&gt; (item=leaf2)

TASK [TASK 1.2. Wait for virtual routers to finish booting.] *******************************************************************************************************************************************
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (15 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (14 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (13 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (12 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (11 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (10 retries left).
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (9 retries left).
ok: [localhost] =&gt; (item=spine1)
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (15 retries left).
ok: [localhost] =&gt; (item=spine2)
ok: [localhost] =&gt; (item=leaf1)
FAILED - RETRYING: TASK 1.2. Wait for virtual routers to finish booting. (15 retries left).
ok: [localhost] =&gt; (item=leaf2)

TASK [TASK 1.3. Auto discover device IPs and add to inventory group.] **********************************************************************************************************************************
ok: [localhost] =&gt; (item=spine1)
ok: [localhost] =&gt; (item=spine2)
ok: [localhost] =&gt; (item=leaf1)
ok: [localhost] =&gt; (item=leaf2)

TASK [TASK 1.4. Dynamically create hosts.yml inventory.] ***********************************************************************************************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************************************************************************************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
</code></pre>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>Adding Ansible <code>until</code> loop to your toolset will open some new possibilities. Ability to dynamically repeat polling until certain condition is met is powerful and will allow you to add logic to your Playbooks that otherwise might be difficult to achieve.</p>
<p>I hope that my examples helped in illustrating the value of the <code>until</code> loop and you found this post useful.</p>
<p>Thanks for reading!</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Ansible docs for 'until' loop: <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#retrying-a-task-until-a-condition-is-met" target="_blank">https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#retrying-a-task-until-a-condition-is-met</a></li>
<li>Ansible docs for 'wait_for' module:<br>
<a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/wait_for_module.html" target="_blank">https://docs.ansible.com/ansible/latest/collections/ansible/builtin/wait_for_module.html</a></li>
<li>vrnetlab: <a href="https://github.com/plajjan/vrnetlab" target="_blank">https://github.com/plajjan/vrnetlab</a></li>
<li>TTL255 post on vrnetlab: <a href="https://ttl255.com/vrnetlab-run-virtual-routers-in-docker-containers/" target="_blank">https://ttl255.com/vrnetlab-run-virtual-routers-in-docker-containers/</a></li>
<li>GitHub repo with resources for this post: <a href="https://github.com/progala/ttl255.com/tree/master/ansible/until-loop" target="_blank">https://github.com/progala/ttl255.com/tree/master/ansible/until-loop</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Jinja2 Tutorial - Part 6 - Include and Import]]></title><description><![CDATA[In this post we're discussing import and include statements. We learn how these help us better structure our templates and make them easier to maintain.]]></description><link>https://ttl255.com/jinja2-tutorial-part-6-include-and-import/</link><guid isPermaLink="false">5fa846321e69ff52c5b06615</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[automation]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 08 Nov 2020 20:45:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Welcome to another instalment in my Jinja2 Tutorial series. So far we've learned a lot about rendering, control structures and various functions. Here we'll start discussing language features that help us deal with organizing templates. First constructs we'll look at are <code>include</code> and <code>import</code> statements.</p>
<h2 id="jinja2tutorialseries">Jinja2 Tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/jinja2-tutorial-part-1-introduction-and-variable-substitution/">Jinja2 Tutorial - Part 1 - Introduction and variable substitution</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-2-loops-and-conditionals/">Jinja2 Tutorial - Part 2 - Loops and conditionals</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-3-whitespace-control/">Jinja2 Tutorial - Part 3 - Whitespace control</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-4-template-filters/">Jinja2 Tutorial - Part 4 - Template filters</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-5-macros/">Jinja2 Tutorial - Part 5 - Macros</a></li>
<li>Jinja2 Tutorial - Part 6 - Include and Import</li>
<li><a href="https://ttl255.com/j2live-online-jinja2-parser/">J2Live - Online Jinja2 Parser</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a>
<ul>
<li><a href="#purpose-and-synt">Purpose and syntax</a></li>
<li><a href="#using-include-to">Using 'include' to split up large templates</a></li>
<li><a href="#shared-template">Shared template snippets with 'include'</a></li>
<li><a href="#missing-and-alte">Missing and alternative templates</a></li>
</ul>
</li>
<li><a href="#import-statement">Import statement</a>
<ul>
<li><a href="#three-ways-of-im">Three ways of importing</a></li>
<li><a href="#caching-and-cont">Caching and context variables</a></li>
<li><a href="#disabling-macro">Disabling macro caching</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p6-import-include" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="introduction"></a></p>
<h2 id="introduction">Introduction</h2>
<p><code>Include</code> and <code>Import</code> statements are some of the tools that Jinja gives us to help with organizing collections of templates, especially once these grow in size.</p>
<p>By using these constructs we can split templates into smaller logical units, leading to files with well-defined scopes. This in turn will make it easier to modify templates when new requirements come up.</p>
<p>The end goal of well-structured collection of templates is increased re-usability as well as maintainability.</p>
<p><a name="purpose-and-synt"></a></p>
<h3 id="purposeandsyntax">Purpose and syntax</h3>
<p>'Include' statement allows you to break large templates into smaller logical units that can then be assembled in the final template.</p>
<p>When you use <code>include</code> you refer to another template and tell Jinja to render the referenced template. Jinja then inserts rendered text into the current template.</p>
<p>Syntax for <code>include</code> is:</p>
<p><code>{% include 'path_to_template_file' %}</code></p>
<p>where 'path_to_template_file' is the full path to the template which we want included.</p>
<p>For instance, below we have template named <code>cfg_draft.j2</code> that tells Jinja to find template named <code>users.j2</code>, render it, and replace <code>{% include ... %}</code> block with rendered text.</p>
<p><code>cfg_draft.j2</code></p>
<pre><code class="language-text">{% include 'users.j2' %}
</code></pre>
<p><code>users.j2</code></p>
<pre><code class="language-text">username przemek privilege 15 secret NotSoSecret
</code></pre>
<p>Final result:</p>
<pre><code class="language-text">username przemek privilege 15 secret NotSoSecret
</code></pre>
<p><a name="using-include-to"></a></p>
<h3 id="usingincludetosplituplargetemplates">Using 'include' to split up large templates</h3>
<p>If you look at typical device configuration, you will see a number of sections corresponding to given features. You might have interface configuration section, routing protocol one, access-lists, routing policies, etc. We could write single template generating this entire configuration:</p>
<p><code>device_config.j2</code></p>
<pre><code class="language-text">hostname {{ hostname }}

banner motd ^
===========================================
|   This device is property of BigCorpCo  |
|   Unauthorized access is unauthorized   |
|  Unless you're authorized to access it  |
|  In which case play nice and stay safe  |
===========================================
^

no ip domain lookup
ip domain name local.lab
ip name-server {{ name_server_pri }}
ip name-server {{ name_server_sec }}

ntp server {{ ntp_server_pri }} prefer
ntp server {{ ntp_server_sec }}

{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 {{ idata.description }}
 {{ idata.ipv4_address }}
{% endfor %}

{% for pl_name, pl_lines in prefix_lists.items() -%}
ip prefix-list {{ pl_name }}
{%- for line in pl_lines %}
 {{ line -}}
{%  endfor -%}
{% endfor %}

router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers %}
 neighbor {{ peer.ip }} remote-as {{ peer.as_no }}
 neighbor {{ peer.ip }} description {{ peer.description }}
{%- endfor %}
</code></pre>
<p>Often we grow our templates organically and add one section after another to the single template responsible for generating configuration. Over time however this template grows too large and it becomes difficult to maintain it.</p>
<p>One way of dealing with the growing complexity is to identify sections that roughly correspond to single feature. Then we can move them into their own templates that will be included in the final one.</p>
<p>What we're aiming for is a number of smaller templates dealing with clearly defined feature configuration sections. That way it's easier to locate file to modify when you need to make changes. It's also easier to tell which template does what since we can now give them appropriate names like &quot;bgp.j2&quot; and &quot;acls.j2&quot;, instead of having one big template named &quot;device_config.j2&quot;.</p>
<p>Taking the previous template we can decompose it into smaller logical units:</p>
<p><code>base.j2</code></p>
<pre><code class="language-text">hostname {{ hostname }}

banner motd ^
{% include 'common/banner.j2' %}
^
</code></pre>
<p><code>dns.j2</code></p>
<pre><code class="language-text">no ip domain lookup
ip domain name local.lab
ip name-server {{ name_server_pri }}
ip name-server {{ name_server_sec }}
</code></pre>
<p><code>ntp.j2</code></p>
<pre><code class="language-text">ntp server {{ ntp_server_pri }} prefer
ntp server {{ ntp_server_sec }}
</code></pre>
<p><code>interfaces.j2</code></p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 {{ idata.description }}
 {{ idata.ipv4_address }}
{% endfor %}
</code></pre>
<p><code>prefix_lists.j2</code></p>
<pre><code class="language-text">{% for pl_name, pl_lines in prefix_lists.items() -%}
ip prefix-list {{ pl_name }}
{%- for line in pl_lines %}
 {{ line -}}
{%  endfor -%}
{% endfor %}
</code></pre>
<p><code>bgp.j2</code></p>
<pre><code class="language-text">router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers %}
 neighbor {{ peer.ip }} remote-as {{ peer.as_no }}
 neighbor {{ peer.ip }} description {{ peer.description }}
{%- endfor %}
</code></pre>
<p>We now have a collection of separate templates, each with a clear name conveying its purpose. While our example doesn't have too many lines, I think you'd agree with me that the logical grouping we arrived at will be easier to work with and it'll be quicker to build mental model of what is going on here.</p>
<p>With features moved to individual templates we can finally use <code>include</code> statements to compose our final config template:</p>
<p><code>config_final.j2</code></p>
<pre><code class="language-text">{# Hostname and banner -#}
{% include 'base.j2' %}

{% include 'dns.j2' %}

{% include 'ntp.j2' %}

{% include 'interfaces.j2' %}

{% include 'prefix_lists.j2' %}

{# BGP instance and peering -#}
{% include 'bgp.j2' %}
</code></pre>
<p>You open this template and just from a quick glance you should be able to get an idea as to what it's trying to do. It's much cleaner, and we can easily add comments that won't get lost among hundreds of other lines.</p>
<p>As a bonus, you can quickly test template with one feature disabled by commenting single line out, or simply temporarily removing it.</p>
<p>It's now easier to make changes and it's faster to identify feature and corresponding template instead of searching through one big template with potentially hundreds of lines.</p>
<p>Similarly, when new section is needed we can create separate template and include it in the final template therefore fulfilling our goal of increasing modularity.</p>
<p><a name="shared-template"></a></p>
<h3 id="sharedtemplatesnippetswithinclude">Shared template snippets with 'include'</h3>
<p>You might have also noticed that one of the included templates had itself <code>include</code> statement.</p>
<p><code>base.j2</code></p>
<pre><code class="language-text">hostname {{ hostname }}

banner motd ^
{% include 'common/banner.j2' %}
^
</code></pre>
<p>You can use <code>include</code> statement at any level in the hierarchy of templates and anywhere in the template you want. This is exactly what we did here; we moved text of our banner to a separate file which we then include in <code>base.j2</code> template.</p>
<p>We could argue that banner itself is not important enough to warrant its own template. However, there's another class of use cases where <code>include</code> is helpful. We can maintain library of common snippets used across many different templates.</p>
<p>This differs from our previous example, where we decomposed one big template into smaller logical units all tightly related to the final template. With common library we have units that are re-usable across many different templates that might not otherwise have any similarities.</p>
<p><a name="missing-and-alte"></a></p>
<h3 id="missingandalternativetemplates">Missing and alternative templates</h3>
<p>Jinja allows us to ask for template to be included <code>optionally</code> by adding <code>ignore missing</code> argument to <code>include</code>.</p>
<p><code>{% include 'guest_users.j2' ignore missing %}</code></p>
<p>It essentially tells Jinja to look for <code>guest_users.j2</code> template and insert rendered text if found. If template is not found this will result in blank line, but no error will be raised.</p>
<p>I would generally advise against using this in your templates. It's not something that's widely used so someone reading your template might not know what it's meant to do. End result also relies on the presence of the specific file which might make troubleshooting more difficult.</p>
<p>There are better ways of dealing with optional features, some of which rely on template inheritance that we will talk about in the next post.</p>
<p>Closely related to 'ignore missing' is possibility of providing list of templates to include. Jinja will check templates for existence, including the first one that exists.</p>
<p>In the below example, if <code>local_users.j2</code> does not exist but <code>radius_users.j2</code> does, then rendered <code>radius_users.j2</code> will end up being inserted.</p>
<p><code>{% include ['local_users.j2', 'radius_users.j2'] %}</code></p>
<p>You can even combine list of templates with <code>ignore missing</code> argument:</p>
<p><code>{% include ['local_users.j2', 'radius_users.j2'] ignore missing %}</code></p>
<p>This will result in search for listed templates and no error raised if none of them are found.</p>
<p>Again, while tempting, I'd advise against using this feature unless you exhausted other avenues. I wouldn't enjoy having to figure out which one of the listed templates ended up being included if something didn't look right in my final render.</p>
<p>To summarize, you can use 'include' to:</p>
<ul>
<li>split large template into smaller logical units</li>
<li>re-use snippets shared across multiple templates</li>
</ul>
<p><a name="import-statement"></a></p>
<h2 id="importstatement">Import statement</h2>
<p>In Jinja we use <code>import</code> statement to access macros kept in other templates. The idea is to have often used macros in their own files that are then imported in the templates that need them.</p>
<p>This is different than <code>include</code> statement in that no rendering takes place here. Instead the way it works is very similar to import statement in Python. Imported macros are made available for use in the template that imports them.</p>
<p><a name="three-ways-of-im"></a></p>
<h3 id="threewaysofimporting">Three ways of importing</h3>
<p>There are three ways in which we can import macros.</p>
<p>All three ways will use import the below template:</p>
<p><code>macros/ip_funcs.j2</code></p>
<pre><code class="language-text">{% macro ip_w_wc(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('hostmask')}}
{%- endmacro -%}

{% macro ip_w_netm(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('netmask') }}
{%- endmacro -%}

{% macro ip_w_pfxlen(ip_net) -%}
{{ ip_net|ipaddr('network/prefix') }}
{%- endmacro -%}
</code></pre>
<ol>
<li>
<p>Importing the whole template and assigning it to variable. Macros are attributes of the variable.</p>
<p><code>imp_ipfn_way1.j2</code></p>
<pre><code class="language-text">{% import 'macros/ip_funcs.j2' as ipfn %}

{{ ipfn.ip_w_wc('10.0.0.0/24') }}
{{ ipfn.ip_w_netm('10.0.0.0/24') }}
{{ ipfn.ip_w_pfxlen('10.0.0.0/24') }}
</code></pre>
</li>
<li>
<p>Importing specific macro into the current namespace.</p>
<p><code>imp_ipfn_way2.j2</code></p>
<pre><code class="language-text">{% from 'macros/ip_funcs.j2' import ip_w_wc, ip_w_pfxlen %}

{{ ip_w_wc('10.0.0.0/24') }}
{{ ip_w_pfxlen('10.0.0.0/24') }}
</code></pre>
</li>
<li>
<p>Importing specific macro into the current namespace and giving it an alias.</p>
<p><code>imp_ipfn_way3</code></p>
<pre><code class="language-text">{% from 'macros/ip_funcs.j2' import ip_w_wc as ipwild %}

{{ ipwild('10.0.0.0/24') }}
</code></pre>
</li>
</ol>
<p>You can also combine 2 with 3:</p>
<p><code>imp_ipfn_way2_3</code></p>
<pre><code class="language-text">{% from 'macros/ip_funcs.j2' import ip_w_wc as ipwild, ip_w_pfxlen %}

{{ ipwild('10.0.0.0/24') }}
{{ ip_w_pfxlen('10.0.0.0/24') }}
</code></pre>
<p>My recommendation is to always use <code>1</code>. This forces you to access macros via explicit namespace. Methods <code>2</code> and <code>3</code> risk clashes with variables and macros defined in the current namespace. As is often the case in Jinja, explicit is better than implicit.</p>
<p><a name="caching-and-cont"></a></p>
<h3 id="cachingandcontextvariables">Caching and context variables</h3>
<p>Imports are cached, which means that they are loaded very quickly on each subsequent use. There is a price to pay for that, namely the imported templates don't have access to variables in template that imports them.</p>
<p>This means that by default you can't access variables passed into the context inside of macros imported from another file.</p>
<p>Instead you have to build your macros so that they only rely on values passed to them explicitly.</p>
<p>To illustrate this I wrote two versions of macro named <code>def_if_desc</code>, one trying to access variables available to template importing it. The other macro relies on dictionary passed to it explicitly via value.</p>
<p>Both versions use the below data:</p>
<p><code>default_desc.yaml</code></p>
<pre><code class="language-text">interfaces:
 Ethernet10:
   role: desktop
</code></pre>
<ul>
<li>
<p>Version accessing template variables:</p>
<p><code>macros/def_desc_ctxvars.j2</code></p>
<pre><code class="language-text">{% macro def_if_desc(ifname) -%}
Unused port, dedicated to {{ interfaces[ifname].role }} devices
{%- endmacro -%}
</code></pre>
<p><code>im_defdesc_vars.j2</code></p>
<pre><code class="language-text">{% import 'macros/def_desc_ctxvars.j2' as desc -%}

{{ desc.def_if_desc('Ethernet10') }}
</code></pre>
<p>When I try to render <code>im_defdesc_vars.j2</code> I get the below traceback:</p>
<pre><code class="language-text">...(cut for brevity)
  File &quot;F:\projects\j2-tutorial\templates\macros\def_desc_ctxvars.j2&quot;, line 2, in template
    Unused port, dedicated to {{ interfaces[ifname].descri }} devices
  File &quot;F:\projects\j2-tutorial\venv\lib\site-packages\jinja2\environment.py&quot;, line 452, in getitem
    return obj[argument]
jinja2.exceptions.UndefinedError: 'interfaces' is undefined
</code></pre>
<p>You can see that Jinja complains that it cannot access <code>interfaces</code>. This is just as we expected.</p>
</li>
<li>
<p>Version accessing key of dictionary passed explicitly by importing template.</p>
<p><code>default_desc.j2</code></p>
<pre><code class="language-text">{% macro def_if_desc(intf_data) -%}
Unused port, dedicated to {{ intf_data.role }} devices
{%- endmacro -%}
</code></pre>
<p><code>im_defdesc.j2</code></p>
<pre><code class="language-text">{% import 'macros/default_desc.j2' as desc -%}

{{ desc.def_if_desc(interfaces['Ethernet10']) }}
</code></pre>
<p>And this renders just fine:</p>
<pre><code class="language-text">Unused port, dedicated to desktop devices
</code></pre>
</li>
</ul>
<p>Hopefully now you can see what the default behavior is when importing macros. Since values of variables in the context can change at any time, Jinja engine cannot cache them and we are not allowed to access them from within macros.</p>
<p><a name="disabling-macro"></a></p>
<h3 id="disablingmacrocaching">Disabling macro caching</h3>
<p>However, if for whatever reason you think it is a good idea to allow your macros to access context variables you can change the default behavior with additional argument <code>with context</code> which you pass to <code>import</code> statement.</p>
<p><code>Note: This will automatically disable caching.</code></p>
<p>For completeness this is how we can &quot;fix&quot; our failing macro:</p>
<p><code>macros/def_desc_ctxvars.j2</code></p>
<pre><code class="language-text">{% macro def_if_desc(ifname) -%}
Unused port, dedicated to {{ interfaces[ifname].role }} devices
{%- endmacro -%}
</code></pre>
<p><code>im_defdesc_vars_wctx.j2</code></p>
<pre><code class="language-text">{% import 'macros/def_desc_ctxvars.j2' as desc with context -%}

{{ desc.def_if_desc('Ethernet10') }}
</code></pre>
<p>And now it works:</p>
<pre><code class="language-text">Unused port, dedicated to devices
</code></pre>
<p>Personally, I don't think it's a good idea to use <code>import</code> together <code>with context</code>. The whole point of importing macros from separate file is to allow them to be used in other templates and leveraging caching. There could be potentially hundreds, if not thousands, of them, and as soon as you use <code>with context</code> the caching is gone.</p>
<p>I can also see some very subtle bugs creeping in in macros that rely on accessing variables from template context.</p>
<p>To be on the safe side, I'd say stick with standard import and always use namespaces, e.g.</p>
<p><code>{% import 'macros/ip_funcs.j2' as ipfn %}</code></p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>We learned about two Jinja constructs that can help us in managing complexity emerging when our templates grow in size. By leveraging <code>import</code> and <code>include</code> statements we can increase re-usability and make our templates easier to maintain. I hope that included examples showed you how you can use this knowledge to make your template collection better organized and easier to understand.</p>
<p>Here's my quick summary of what to use when:</p>
<table>
<thead>
<tr>
<th></th>
<th>Import</th>
<th>Include</th>
</tr>
</thead>
<tbody>
<tr>
<td>Purpose</td>
<td>Imports macros from other templates</td>
<td>Renders other template and inserts results</td>
</tr>
<tr>
<td>Context variables</td>
<td>Not accessible (default)</td>
<td>Accessible</td>
</tr>
<tr>
<td>Good for</td>
<td>Creating shared macro libraries</td>
<td>Splitting template into logical units and common snippets</td>
</tr>
</tbody>
</table>
<p>I hope you found this post useful and are looking forward to more. Next post will continue discussion of ways to organize templates by focusing on template inheritance. Stay tuned!</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Jinja2 import, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#import" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/templates/#import</a></li>
<li>Jinja2 include, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#include" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/templates/#include</a></li>
<li>GitHub repo with resources for this post. Available at: <a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p6-import-include" target="_blank">https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p6-import-include</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Jinja2 Tutorial - Part 5 - Macros]]></title><description><![CDATA[In this post we look at Jinja macros. We learn what macros are and how to use them by following many examples illustrating different use case scenarios.]]></description><link>https://ttl255.com/jinja2-tutorial-part-5-macros/</link><guid isPermaLink="false">5f7a389b1e69ff52c5b06601</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Mon, 05 Oct 2020 18:37:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Welcome to the part 5 of Jinja2 Tutorial where we learn all about macros. We'll talk about what macros are, why we would use them and we'll see some examples to help us appreciate this feature better.</p>
<h2 id="jinja2tutorialseries">Jinja2 Tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/jinja2-tutorial-part-1-introduction-and-variable-substitution/">Jinja2 Tutorial - Part 1 - Introduction and variable substitution</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-2-loops-and-conditionals/">Jinja2 Tutorial - Part 2 - Loops and conditionals</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-3-whitespace-control/">Jinja2 Tutorial - Part 3 - Whitespace control</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-4-template-filters/">Jinja2 Tutorial - Part 4 - Template filters</a></li>
<li>Jinja2 Tutorial - Part 5 - Macros</li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-6-include-and-import/">Jinja2 Tutorial - Part 6 - Include and Import</a></li>
<li><a href="https://ttl255.com/j2live-online-jinja2-parser/">J2Live - Online Jinja2 Parser</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#what-are-macros">What are macros?</a></li>
<li><a href="#why-and-how-of-m">Why and how of macros</a></li>
<li><a href="#adding-parameter">Adding parameters</a></li>
<li><a href="#macros-for-deepl">Macros for deeply nested structures</a></li>
<li><a href="#branching-out-in">Branching out inside macro</a></li>
<li><a href="#macros-in-macros">Macros in macros</a></li>
<li><a href="#moving-macros-to">Moving macros to a separate file</a></li>
<li><a href="#advanced-macro-u">Advanced macro usage</a>
<ul>
<li><a href="#varargs-and-kwar">Varargs and kwargs</a></li>
<li><a href="#call-block"><code>call</code> block</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p5-macros" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="what-are-macros"></a></p>
<h2 id="whataremacros">What are macros?</h2>
<p>Macros are similar to functions in many programming languages. We use them to encapsulate logic used to perform repeatable actions. Macros can take arguments or be used without them.</p>
<p>Inside of macros we can use any of the Jinja features and constructs. Result of running macro is some text. You can essentially treat macro as one big evaluation statement that also allows parametrization.</p>
<p><a name="why-and-how-of-m"></a></p>
<h2 id="whyandhowofmacros">Why and how of macros</h2>
<p>Macros are great for creating reusable components when we find ourselves copy pasting around same lines of text and code. You might benefit from macro even when all it does is rendering static text.</p>
<p>Take for example device banners, these tend to be static but are used over and over again. Instead of copy pasting text of the banner across your templates you can create macro and have it render the banner.</p>
<p>Not only will you reduce mistakes that can happen during copying but you also make future updates to the banner much easier. Now you have only one place where the banner needs to be changed and anything else using this macro will reflect the changes automatically.</p>
<pre><code class="language-text">{% macro banner() -%}
banner motd ^
===========================================
|   This device is property of BigCorpCo  |
|   Unauthorized access is unauthorized   |
|  Unless you're authorized to access it  |
|  In which case play nice and stay safe  |
===========================================
^
{% endmacro -%}

{{ banner() }}
</code></pre>
<pre><code class="language-text">banner motd ^
===========================================
|   This device is property of BigCorpCo  |
|   Unauthorized access is unauthorized   |
|  Unless you're authorized to access it  |
|  In which case play nice and stay safe  |
===========================================
^
</code></pre>
<p>So that's our first macro right there!</p>
<p>As you can see above we start macro with <code>{% macro macro_name(arg1, arg2) %}</code> and we end it with <code>{% endmacro %}</code>. Arguments are optional.</p>
<p>Anything you put in between opening and closing tags will be processed and rendered at a location where you called the macro.</p>
<p>Once we defined macro we can use it anywhere in our template. We can directly insert results by using <code>{{ macro_name() }}</code> substitution syntax. We can also use it inside other constructs like <code>if..else</code> blocks or <code>for</code> loops. You can even pass macros to other macros!</p>
<p><a name="adding-parameter"></a></p>
<h2 id="addingparameters">Adding parameters</h2>
<p>Real fun begins when you start using parameters in your macros. That's when their show their true potential.</p>
<p>Oure next macro renders default interface description. We assign different roles to our ports and we want the default description to reflect that. We could achieve this by writing macro taking interface role as argument.</p>
<p>Data:</p>
<pre><code class="language-text">interfaces:
 - name: Ethernet10
   role: desktop
 - name: Ethernet11
   role: desktop
 - name: Ethernet15
   role: printer
 - name: Ethernet22
   role: voice
</code></pre>
<p>Template with macro:</p>
<pre><code class="language-text">{% macro def_if_desc(if_role) -%}
Unused port, dedicated to {{ if_role }} devices
{%- endmacro -%}

{% for intf in interfaces -%}
interface {{ intf.name }}
  description {{ def_if_desc(intf.role) }}
{% endfor -%}
</code></pre>
<p>Rendered text:</p>
<pre><code class="language-text">interface Ethernet10
  description Unused port, dedicated to desktop devices
  ip address
interface Ethernet11
  description Unused port, dedicated to desktop devices
  ip address
interface Ethernet15
  description Unused port, dedicated to printer devices
  ip address
interface Ethernet22
  description Unused port, dedicated to voice devices
  ip address
</code></pre>
<p>It might not be immediately apparent if macro is useful here since we only have one line in the body. We could've just written this line inside of the <code>for</code> loop. Downside of that is that our intent is not clearly conveyed.</p>
<pre><code class="language-text">{% for intf in interfaces -%}
interface {{ intf.name }}
  description Unused port, dedicated to {{ intf.role }} devices
{% endfor -%}
</code></pre>
<p>This works but it's not immediately obvious that this is description we want to be used as a default. Things will get even worse if we start adding more processing here.</p>
<p>If we use macro however, the name of the macro tells us clearly that a default interface description will be applied. That is, it is clear what our intent was here.</p>
<p>And there's the real kicker. Macros can be moved to separate files and included in templates that need them. Which means you only need to maintain this one macro that then can be used by hundreds of templates! And number of places you have to update your default description? One, just one.</p>
<p><a name="macros-for-deepl"></a></p>
<h2 id="macrosfordeeplynestedstructures">Macros for deeply nested structures</h2>
<p>Another good use case for macros is accessing values in deeply nested data structures.</p>
<p>Modern APIs can return results with many levels of dictionaries and lists making it easy to make error when writing expressions accessing values in these data structures.</p>
<p>The below is real-life example of output returned by Arista device for command:</p>
<p><code>sh ip bgp neighbors x.x.x.x received-routes | json</code></p>
<p>Due to size, I'm showing full result for one route entry only, out of 3:</p>
<pre><code class="language-text">{
  &quot;vrfs&quot;: {
      &quot;default&quot;: {
      &quot;routerId&quot;: &quot;10.3.0.2&quot;,
      &quot;vrf&quot;: &quot;default&quot;,
      &quot;bgpRouteEntries&quot;: {
          &quot;10.1.0.1/32&quot;: {
          &quot;bgpAdvertisedPeerGroups&quot;: {},
          &quot;maskLength&quot;: 32,
          &quot;bgpRoutePaths&quot;: [
              {
              &quot;asPathEntry&quot;: {
                  &quot;asPathType&quot;: null,
                  &quot;asPath&quot;: &quot;i&quot;
              },
              &quot;med&quot;: 0,
              &quot;localPreference&quot;: 100,
              &quot;weight&quot;: 0,
              &quot;reasonNotBestpath&quot;: null,
              &quot;nextHop&quot;: &quot;10.2.0.0&quot;,
              &quot;routeType&quot;: {
                  &quot;atomicAggregator&quot;: false,
                  &quot;suppressed&quot;: false,
                  &quot;queued&quot;: false,
                  &quot;valid&quot;: true,
                  &quot;ecmpContributor&quot;: false,
                  &quot;luRoute&quot;: false,
                  &quot;active&quot;: true,
                  &quot;stale&quot;: false,
                  &quot;ecmp&quot;: false,
                  &quot;backup&quot;: false,
                  &quot;ecmpHead&quot;: false,
                  &quot;ucmp&quot;: false
              }
              }
          ],
          &quot;address&quot;: &quot;10.1.0.1&quot;
          },
  ...
      &quot;asn&quot;: &quot;65001&quot;
      }
  }
}
</code></pre>
<p>There's a lot going on here and in most cases you will only need to get values for few of these attributes.</p>
<p>Say we wanted to access just prefix, next-hop and validity of the path.</p>
<p>Below is object hierarchy we need to navigate in order to access these values:</p>
<ul>
<li><code>vrfs.default.bgpRouteEntries</code> - prefixes are here (as keys)</li>
<li><code>vrfs.default.bgpRouteEntries[pfx].bgpRoutePaths.0.nextHop</code> - next hop</li>
<li><code>vrfs.default.bgpRouteEntries[pfx].bgpRoutePaths.0.routeType.valid</code> - route validity</li>
</ul>
<p>I don't know about you but I really don't fancy copy pasting that into all places I would need to access these.</p>
<p>So here's what we can do to make it a bit easier, and more obvious, for ourselves.</p>
<pre><code class="language-text">{% macro print_route_info(sh_bgpr) -%}
{% for route, routenfo in vrfs.default.bgpRouteEntries.items() -%}
Route: {{ route }} - Next Hop: {{ 
routenfo.bgpRoutePaths.0.nextHop }} - Permitted: {{ 
routenfo.bgpRoutePaths.0.routeType.valid }}
{% endfor %}
{%- endmacro -%}

{{ print_route_info(sh_bgp_routes) }}
</code></pre>
<pre><code class="language-text">Route: 10.1.0.1/32 - Next Hop: 10.2.0.0 - Permitted: True
Route: 10.1.0.2/32 - Next Hop: 10.2.0.0 - Permitted: True
Route: 10.1.0.3/32 - Next Hop: 10.2.0.0 - Permitted: True
</code></pre>
<p>I moved the logic, and complexity, involved in accessing attributes to a macro called <code>print_route_info</code>. This macro takes output of our show command and then gives us back only what we need.</p>
<p>If we need to access more attributes we'd only have to make changes to the body of our macro.</p>
<p>At the place where we actually need the information we call well named macro and give it the output of the command. This makes it more obvious as to what we're trying to achieve and mechanics of navigating data structures are hidden away.</p>
<p><a name="branching-out-in"></a></p>
<h2 id="branchingoutinsidemacro">Branching out inside macro</h2>
<p>Let's do another example, this time our macro will have <code>if..else</code> block to show that we can return result depending on conditional checks.</p>
<p>I created data model where BGP peer IP and name are not explicitly listed in the mapping I use for specifying peers. Instead we're pointing each peer entry to local interface over which we want to establish the peering.</p>
<p>We're also assuming here that all of our peerings use /31 mask.</p>
<pre><code class="language-text">interfaces:
  Ethernet1:
    ip_add: 10.1.1.1/31
    peer: spine1
    peer_intf: Ethernet1
  Ethernet2:
    ip_add: 10.1.1.9/31
    peer: spine2
    peer_intf: Ethernet1

bgp:
  as_no: 65001
  peers:
    - intf: Ethernet1
      as_no: 64512
    - intf: Ethernet2
      as_no: 64512
</code></pre>
<p>Using this data model we want to build config for BGP neighbors. Taking advantage of <code>ipaddr</code> filter we can do the following:</p>
<ul>
<li>Find 1st IP address in network configured on the linked interface.</li>
<li>Check if 1st IP address equals IP address configured on the interface.
<ul>
<li>If it is equal then IP of BGP peer must be the 2nd IP address in this /31.</li>
<li>If not then BGP peer IP must be the 1st IP address.</li>
</ul>
</li>
</ul>
<p>Converting this to Jinja syntax we get the following:</p>
<pre><code class="language-text">router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers -%}
{% set fst_ip = interfaces[peer.intf].ip_add | ipaddr(0) -%}
{% if our_ip == fst_ip -%}
{% set peer_ip = fst_ip | ipaddr(1) | ipaddr('address') -%}
{% else -%}
{% set peer_ip = fst_ip | ipaddr('address') -%}
{% endif %}
 neighbor {{ peer_ip }} remote-as {{ peer.as_no }}
 neighbor {{ peer_ip }} description {{ interfaces[peer.intf].peer }}
{%- endfor %}
</code></pre>
<p>And this is the result of rendering:</p>
<pre><code class="language-text">router bgp 65001
 neighbor 10.1.1.0 remote-as 64512
 neighbor 10.1.1.0 description spine1
 neighbor 10.1.1.8 remote-as 64512
 neighbor 10.1.1.8 description spine2
</code></pre>
<p>Job done. We got what we wanted, neighbor IP worked out automatically from IP assigned to local interface.</p>
<p>But, the longer I look at it the more I don't like feel of this logic and manipulation preceding the actual neighbor statements.</p>
<p>You also might want to use the logic of working out peer IP elsewhere in the template which means copy pasting. And if later you change mask on the interface or want to change data structure slightly you'll have to find all the places with the logic and make sure you change all of them.</p>
<p>I'd say this case is another good candidate for building a macro.</p>
<p>So I'm going to move logic for working out peer IP to the macro that I'm calling <code>peer_ip</code>. This macro will take one argument <code>local_intf</code> which is the name of the interface for which we're configuring peering.</p>
<p>If you compare this version with non-macro version you can see that most of the code is the same except that instead of setting final value and assigning it to variable we use substitution statements.</p>
<pre><code class="language-text">{% macro peer_ip(local_intf) -%}
{% set local_ip = interfaces[local_intf].ip_add -%}
{% set fst_ip = local_ip | ipaddr(0) -%}
{% if fst_ip == local_ip -%}
{{ fst_ip | ipaddr(1) | ipaddr('address') -}}
{% else -%}
{{ fst_ip | ipaddr('address') -}}
{%- endif -%}
{% endmacro -%}

router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers -%}
{%- set bgp_peer_ip = peer_ip(peer.intf) %}
 neighbor {{ bgp_peer_ip }} remote-as {{ peer.as_no }}
 neighbor {{ bgp_peer_ip }} description {{ interfaces[peer.intf].peer }}
{%- endfor %}
</code></pre>
<p>We use this macro in exactly one place in our function, we assign value it returns to variable <code>bgp_peer_ip</code>. We can then use <code>bgp_peer_ip</code> in our neighbor statements.</p>
<p><code>{%- set bgp_peer_ip = peer_ip(peer.intf) %}</code></p>
<p>Another thing that I like about this approach is that we can move macro to its own file and then include it in the templates that use it.</p>
<p>We'll talk about Jinja imports and includes in more details in future posts. However this is such a useful feature that later in this post I will show you short example of macros in their own files.</p>
<p><a name="macros-in-macros"></a></p>
<h2 id="macrosinmacros">Macros in macros</h2>
<p>Now here's an interesting one. We can pass macros as arguments to other macros. This is similar to Python where functions can be passed around like any other object.</p>
<p>Would we want to do it though? There are certainly cases when that might be useful. I can think of need for having more generic macro producing some result and taking another macro as an argument to enable changing of format used to render result.</p>
<p>This means that we could have parent macro deal with rendering common part of the output we're interested in. Then macro passed as an argument would be responsible for handling difference in rendering specific bit that would be dependent on the needs of the caller.</p>
<p>To illustrate this and make it easier to visualize, consider case of rendering ACL entries. Different vendors could, and often do, use different format for IP source and destination objects. Some will use &quot;net_address/pfxlen&quot;, while some will use &quot;net_address wildcard&quot;.</p>
<p>We could write multiple ACL rendering macros, one for each case. Another option would be to have <code>if..else</code> logic in larger macro with macro argument deciding which format to use.</p>
<p>Or we can encapsulate logic responsible for format conversion in tiny macros. We then can have macro responsible for ACL rendering that receives format conversion macro as one of the arguments. That is, ACL macro doesn't know how to do rendering and it does not care. It just knows that it will be given macro from outside and that it can apply it where required.</p>
<p>Here's the actual implementation that includes 3 different formatting macros.</p>
<p>Data used for our example:</p>
<pre><code class="language-text">networks:
  - name: quant_server_net
    prefix: 10.0.0.0/24
    services:
      - computing

svc_def:
  computing:
    - {ip: 10.90.0.5/32, prot: tcp, port: 5008}
    - {ip: 10.91.4.0/255.255.255.0, prot: tcp, port: 5009}
    - {ip: 10.91.6.32/27, prot: tcp, port: 6800}
</code></pre>
<p>Template with macros:</p>
<pre><code class="language-text">{% macro ip_w_wc(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('hostmask')}}
{%- endmacro -%}

{% macro ip_w_netm(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('netmask') }}
{%- endmacro -%}

{% macro ip_w_pfxlen(ip_net) -%}
{{ ip_net|ipaddr('network/prefix') }}
{%- endmacro -%}

{% macro acl_lines(aclobj, src_pfx, pfx_fmt) -%}
{% for line in aclobj %}
 permit {{ line.prot }} {{ pfx_fmt(src_pfx) }} {{ pfx_fmt(line.ip) }} 
{%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
{% endfor -%}
{%- endmacro -%}

{% for net in networks -%}
ip access-list extended al_{{ net.name }}
{%-  for svc in net.services -%}
{{ acl_lines(svc_def[svc], net.prefix, ip_w_pfxlen) }}
{%  endfor -%}
{% endfor -%}
</code></pre>
<p>Rendering results with <code>ip_w_wc</code> macro:</p>
<pre><code class="language-text">ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0 0.0.0.255 10.90.0.5 0.0.0.0 eq 5008
 permit tcp 10.0.0.0 0.0.0.255 10.91.4.0 0.0.0.255 eq 5009
 permit tcp 10.0.0.0 0.0.0.255 10.91.6.32 0.0.0.31 eq 6800
</code></pre>
<p>Rendering results with <code>ip_w_netm</code> macro:</p>
<pre><code class="language-text">ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0 255.255.255.0 10.90.0.5 255.255.255.255 eq 5008
 permit tcp 10.0.0.0 255.255.255.0 10.91.4.0 255.255.255.0 eq 5009
 permit tcp 10.0.0.0 255.255.255.0 10.91.6.32 255.255.255.224 eq 6800
</code></pre>
<p>Rendering results with <code>ip_w_pfxlen</code> macro:</p>
<pre><code class="language-text">ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0/24 10.90.0.5/32 eq 5008
 permit tcp 10.0.0.0/24 10.91.4.0/24 eq 5009
 permit tcp 10.0.0.0/24 10.91.6.32/27 eq 6800
</code></pre>
<p>Hopefully now you can see what I'm trying to achieve here. I can use the same parent macro in templates rendering the config for different vendors by simply providing different formatter macro. To top it off, we make our intent clear, yet again.</p>
<p>Our formatting macros can be reused in many places and it's very easy to add new formatters that can be used in ACL macro and elsewhere.</p>
<p>Also by decoupling, and abstracting away, IP prefix formatting we make ACL macro more focused.</p>
<p>A lot of these decisions are down to individual preferences but I feel that this technique is very powerful and it's good to know that it's there when you need it.</p>
<p><a name="moving-macros-to"></a></p>
<h2 id="movingmacrostoaseparatefile">Moving macros to a separate file</h2>
<p>I'll now show you an example of how a macro can be moved to a separate template file. We will then import the macro and call it from template located in a completely different file.</p>
<p>I decided to take macros we created for displaying IP network in different formats. I'm moving 3 formatting macros to separate file and keeping ACL macro in the original template.</p>
<p>The result is two templates.</p>
<p><code>ip_funcs.j2</code>:</p>
<pre><code class="language-text">{% macro ip_w_wc(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('hostmask')}}
{%- endmacro -%}

{% macro ip_w_netm(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('netmask') }}
{%- endmacro -%}

{% macro ip_w_pfxlen(ip_net) -%}
{{ ip_net|ipaddr('network/prefix') }}
{%- endmacro -%}
</code></pre>
<p><code>acl_variants.j2</code>:</p>
<pre><code class="language-text">{% import 'ip_funcs.j2' as ipfn -%}

{% macro acl_lines(aclobj, src_pfx, pfx_fmt) -%}
{% for line in aclobj %}
 permit {{ line.prot }} {{ pfx_fmt(src_pfx) }} {{ pfx_fmt(line.ip) }} 
{%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
{% endfor -%}
{%- endmacro -%}

Prefix with prefix length ACL:

{% for net in networks -%}
ip access-list extended al_{{ net.name }}
{%-  for svc in net.services -%}
{{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_wc) }}
{%  endfor -%}
{% endfor %}

Network with Wildcard ACL:

{% for net in networks -%}
ip access-list extended al_{{ net.name }}
{%-  for svc in net.services -%}
{{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_pfxlen) }}
{%  endfor -%}
{% endfor %}

Network with network Mask ACL:

{% for net in networks -%}
ip access-list extended al_{{ net.name }}
{%-  for svc in net.services -%}
{{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_netm) }}
{%  endfor -%}
{% endfor -%}
</code></pre>
<p>First template <code>ip_funcs.j2</code> contains formatter macros, and nothing else. Notice also there's no change to the code, we copied these over ad verbatim.</p>
<p>Something interesting happened to our original template, here called <code>acl_variants.j2</code>. First line <code>{% import 'ip_funcs.j2' as ipfn -%}</code> is new and the way we call formatter macros is different now.</p>
<p>Line <code>{% import 'ip_funcs.j2' as ipfn -%}</code> looks like <code>import</code> statement in Python and it works similarly. Jinja engine will look for file called <code>ip_funcs.j2</code> and will make variables and macros from that file available in namespace <code>ipfn</code>. That is anything found in imported file can be now accessed using <code>ipfn.</code> notation.</p>
<p>And this is how we get to the way we need to call formatters now. For example macro converting IP prefix to network/wildcard form is called with <code>ipfn.ip_w_wc</code> syntax.</p>
<p>For good measure I added all formatting variants to our template and this is the final result:</p>
<pre><code class="language-text">Prefix with prefix length ACL:

ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0 0.0.0.255 10.90.0.5 0.0.0.0 eq 5008
 permit tcp 10.0.0.0 0.0.0.255 10.91.4.0 0.0.0.255 eq 5009
 permit tcp 10.0.0.0 0.0.0.255 10.91.6.32 0.0.0.31 eq 6800


Network with Wildcard ACL:

ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0/24 10.90.0.5/32 eq 5008
 permit tcp 10.0.0.0/24 10.91.4.0/24 eq 5009
 permit tcp 10.0.0.0/24 10.91.6.32/27 eq 6800


Network with network Mask ACL:

ip access-list extended al_quant_server_net
 permit tcp 10.0.0.0 255.255.255.0 10.90.0.5 255.255.255.255 eq 5008
 permit tcp 10.0.0.0 255.255.255.0 10.91.4.0 255.255.255.0 eq 5009
 permit tcp 10.0.0.0 255.255.255.0 10.91.6.32 255.255.255.224 eq 6800

</code></pre>
<p>Moving macros to their own files and importing them from other templates is a very powerful feature. I will be talking more about it in the future post on imports and includes.</p>
<p><a name="advanced-macro-u"></a></p>
<h2 id="advancedmacrousage">Advanced macro usage</h2>
<p><a name="varargs-and-kwar"></a></p>
<h3 id="varargsandkwargs">Varargs and kwargs</h3>
<p>Inside of macros you can access special variables that are exposed by default. Some of them relate to internal plumbing and are not very interesting but few of them you might find use for.</p>
<ul>
<li>
<p><code>varargs</code> - if macro was given more positional arguments than explicitly listed in macro's definition, then Jinja will put them into special variable called <code>varargs</code>. You can then iterate over them and process if you feel it makes sense.</p>
</li>
<li>
<p><code>kwargs</code> - similarly to <code>varargs</code>, any keyword arguments not matching explicitly listed ones will end up in <code>kwargs</code> variable. This can be iterated over using <code>kwargs.items()</code> syntax.</p>
</li>
</ul>
<p>Personally I think both of these are not very useful in most of use cases. In the world of web development it might make sense to accept a number of elements for rendering tables, and other HTML items.</p>
<p>In the world of infrastructure automation I prefer explicit arguments and clear intent which I feel is not the case when using special variables.</p>
<p>I do have some contrived examples to show you how that would work if you ever feel you really can make use of this feature.</p>
<p>Below macro takes one explicit argument <code>vid</code>, which specifies VLAN ID we want assigned as access port to interfaces. Any extra positional arguments will be treated as interface names that need to be configured for the given VLAN ID.</p>
<pre><code class="language-text">{% macro set_access_vlan(vid) -%}
{% for intf in varargs -%}
interface {{ intf }}
  switchport
  switchport mode access
  switchport access vlan {{ vid }}
{% endfor -%}
{%- endmacro -%}

{{ set_access_vlan(10, &quot;Ethernet10&quot;, &quot;Ethernet20&quot;) }}
</code></pre>
<p>Result:</p>
<pre><code class="language-text">interface Ethernet10
  switchport
  switchport mode access
  switchport access vlan 10
interface Ethernet20
  switchport
  switchport mode access
  switchport access vlan 10
</code></pre>
<p>Below is similar macro but this time we have no explicit arguments. We will however read any passed keyword arguments and we treat key as the interface name and value as VLAN ID to assign.</p>
<pre><code class="language-text">{% macro set_access_vlan() -%}
{% for intf, vid in kwargs.items() -%}
interface {{ intf }}
  switchport
  switchport mode access
  switchport access vlan {{ vid }}
{% endfor -%}
{%- endmacro -%}

{{ set_access_vlan(Ethernet10=10, Ethernet15=15, Ethernet20=20) }}
</code></pre>
<p>Render results:</p>
<pre><code class="language-text">interface Ethernet10
  switchport
  switchport mode access
  switchport access vlan 10
interface Ethernet15
  switchport
  switchport mode access
  switchport access vlan 15
interface Ethernet20
  switchport
  switchport mode access
  switchport access vlan 20

</code></pre>
<p>Both of these examples work and even do something potentially useful. These special variables just don't feel right to me but they're there if you ever need them.</p>
<p><a name="call-block"></a></p>
<h3 id="callblock"><code>call</code> block</h3>
<p>Call blocks are constructs that call other macros and are themselves macros, except they have no names, so they're only used at the point they appear.</p>
<p>You use call bocks with <code>{% call called_macro() %}...{% endcal %}</code> syntax.</p>
<p>These work a bit like callbacks since macros they invoke in turn call back to execute <code>call</code> macros. You can see similarities here to our ACL macro that used different formatting macros. We can have many <code>call</code> macros using single named macro, with these <code>call</code> macros allowing variation in logic executed by named macro.</p>
<p>I don't know if there's a historical reason for their existence since I can't really see any advantage of using these over named macros. They are also not very intuitive to use. But again, they're here and maybe you will find need for them.</p>
<p>So contrived example time!</p>
<p>Bellow <code>call</code> macro calls <code>make_acl</code> macro which in turn calls back to execute calling macro:</p>
<pre><code class="language-text">{% macro make_acl(type, name) -%}
ip access-list {{ type }} {{ name }}
{{- caller() }}
{%- endmacro -%}

{% call make_acl('extended', 'al-ext-01') %}
 permit ip 10.0.0.0 0.0.0.255 10.0.0.0.255
 deny ip any any
{%- endcall %}
</code></pre>
<p>Result:</p>
<pre><code class="language-text">ip access-list extended al-ext-01
 permit ip 10.0.0.0 0.0.0.255 10.0.0.0.255
 deny ip any any
</code></pre>
<p>We got some sensible result here, but at what cost? Do you see how ACL lines made it to the body? The magic is in <code>{{ caller() }}</code> line. Here special function <code>caller()</code> essentially executes body of the <code>call</code> block that called <code>make_acl</code> macro.</p>
<p>This is what happened, step by step:</p>
<ul>
<li><code>call</code> launched <code>make_acl</code></li>
<li><code>make_acl</code> worked its way through, rendering stuff, until it encountered <code>caller()</code></li>
<li><code>make_acl</code> executed calling block with <code>caller()</code> and inserted results</li>
<li><code>make_acl</code> moved on past <code>caller()</code> through the rest of its body</li>
</ul>
<p>It works but again I see no advantage over using named macros and passing them explicitly around.</p>
<p>Fun is not over yet though, called macro can invoke caller with arguments.</p>
<pre><code class="language-text">{% macro acl_lines(aclobj, src_pfx) -%}
{% for line in aclobj %}
 permit {{ line.prot }} {{ caller(src_pfx) }} {{ caller(line.ip) }} 
{%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
{% endfor -%}
{%- endmacro -%}

{% for net in networks -%}
ip access-list extended al_{{ net.name }}
{%-  for svc in net.services -%}

{% call(ip_net) acl_lines(svc_def[svc], net.prefix) -%}
{{ ip_net|ipaddr('network/prefix') }}
{%- endcall -%}

{%  endfor -%}
{% endfor -%}
</code></pre>
<p>This is a <code>call</code> block version of our ACL rendering with variable formatters. This time I included formatter inside of the <code>call</code> block. Our block takes <code>ip_net</code> argument which it expects called macro to provide when calling back.</p>
<p>And this is exactly what happens on the below line:</p>
<p><code>permit {{ line.prot }} {{ caller(src_pfx) }} {{ caller(line.ip) }}</code></p>
<p>So, we have <code>call</code> block call <code>acl_lines</code> with two arguments. Macro <code>acl_lines</code> then calls <code>call</code> back with <code>caller(src_pfx)</code> and <code>caller(line.ip)</code> fulfilling its contract.</p>
<p>Caveat here is that we cannot reuse our formatter, it's all in the unnamed macro aka <code>call</code> block. Once it executes, that's it, you need a new one if you want to use formatter.</p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>I think that macros are one of the more powerful features of Jinja and you will benefit greatly from learning how to use them. Combined with <code>import</code> you will get reusable, well defined groups of snippets that can be kept separately from other templates. This allows us to extract repeatable, sometimes complex, logic, and make our templates cleaner and easier to follow.</p>
<p>As always, see what works for you. If your macros get unwieldy consider using custom filters. And be careful when using advanced <code>macro</code> features, these should really be only reserved for special cases, if used at all.</p>
<p>I hope you learned something from this post and that it gave you some ideas. More posts on Jinja2 are coming so do pop by every so often to see what's new :)</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Jinja2 macros, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#macros" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/templates/#macros</a></li>
<li>Jinja2 calls, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#call" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/templates/#call</a></li>
<li>GitHub repo with resources for this post. Available at: <a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p5-macros" target="_blank">https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p5-macros</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Use Python to translate TCP/UDP port numbers to names]]></title><description><![CDATA[Learn how to use Python to translate between TCP/UPD port numbers and their names.]]></description><link>https://ttl255.com/use-python-to-translate-tcp-udp-port-numbers-to-names/</link><guid isPermaLink="false">5f6277261e69ff52c5b065f2</guid><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[automation]]></category><category><![CDATA[Arista]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Wed, 16 Sep 2020 21:00:12 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>This short post shows how you can use Python to convert TCP/UDP port number to port name and vice versa.</p>
<p>Most of us know names of common TCP and UDP ports like 22/ssh, 23/telnet, 80/http or 443/https. We learn these early in our networking careers and many of them are so common that even when woken up middle of the night you'd know 53 is <code>domain</code> aka <code>dns</code>!</p>
<p>But there are also many not-so commonly used ports that have been given names. These ones sometimes show up in firewall logs or are mentioned in literature. Some vendors also try to replace numeric value with a human readable name in the configs and outputs of different commands.</p>
<p>One way or the other, I'd be good to have an easy method of getting port number given its name, and on occasion we might want to get name of particular port number.</p>
<p>There are many ways one could achieve that. We might search web, drop into documentation, or even check <code>/etc/services</code> if we have access to Linux box.</p>
<p>I decided to check if we can do some programmatic translation with Python, seeing as sometimes we could have hundreds of entries to process and I don't fancy doing that by hand.</p>
<p>As it turns out one of the built-in libraries has just what we need. This library is called <code>socket</code>, which has two functions of interest <code>getservbyname</code> and <code>getservbyport</code>. First one translates port name to number, second one does the reverse. Both of them also accept optional protocol name, either <code>tcp</code> or <code>udp</code>. If no protocol is provided we get result as long as there is match for any of these.</p>
<p>Ok, so let's get to it and see some examples.</p>
<pre><code>&gt;&gt;&gt; from socket import getservbyname, getservbyport
&gt;&gt;&gt; getservbyname(&quot;ssh&quot;)
22
&gt;&gt;&gt; getservbyname(&quot;domain&quot;, &quot;udp&quot;)
53
&gt;&gt;&gt; getservbyname(&quot;https&quot;, &quot;tcp&quot;)
443
</code></pre>
<p>Hey, it's working. We can simply use port name but if we want to we can provide protocol name as well.</p>
<p>Naturally we now need to try translating port number to name:</p>
<pre><code>&gt;&gt;&gt; getservbyport(80)
'http'
&gt;&gt;&gt; getservbyport(161, &quot;udp&quot;)
'snmp'
&gt;&gt;&gt; getservbyport(21, &quot;tcp&quot;)
'ftp'
</code></pre>
<p>Again, it's all looking great. Port number on its own translated, same with number and protocol name.</p>
<p>But, there are cases when using just name/number works fine but might fail when protocol is specified as well.</p>
<pre><code>&gt;&gt;&gt; getservbyport(25)
'smtp'

&gt;&gt;&gt; getservbyport(25, &quot;udp&quot;)
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
OSError: port/proto not found

&gt;&gt;&gt; getservbyname(&quot;ntp&quot;)
123

&gt;&gt;&gt; getservbyname(&quot;ntp&quot;, &quot;tcp&quot;)
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
OSError: service/proto not found
</code></pre>
<p>Ah yes, ntp does not use TCP and smtp is not using UDP either. So this makes sense. Just to be on the safe side however, we can use try..except block and prevent accidents from happening.</p>
<pre><code>&gt;&gt;&gt; try:
...     getservbyname(&quot;ntp&quot;, &quot;tcp&quot;)
... except OSError:
...     print(&quot;No matching port number with this protocol&quot;)
... 
No matching port number with this protocol
</code></pre>
<p>But that's not all. There are also rare cases where port can have different name depending on the protocol using it.</p>
<pre><code>&gt;&gt;&gt; getservbyport(21, &quot;udp&quot;)
'fsp'
&gt;&gt;&gt; getservbyport(21, &quot;tcp&quot;)
'ftp'
&gt;&gt;&gt; getservbyport(512, &quot;udp&quot;)
'biff'
&gt;&gt;&gt; getservbyport(512, &quot;tcp&quot;)
'exec'
&gt;&gt;&gt; getservbyname(&quot;who&quot;, &quot;udp&quot;)
513
&gt;&gt;&gt; getservbyname(&quot;login&quot;, &quot;tcp&quot;)
513
&gt;&gt;&gt; getservbyname(&quot;syslog&quot;, &quot;udp&quot;)
514
&gt;&gt;&gt; getservbyname(&quot;shell&quot;, &quot;tcp&quot;)
514
</code></pre>
<p>Funky, same port, but different names! On the bright side these are all the cases that I could find :)</p>
<p>For completeness, same ports without protocol specified:</p>
<pre><code>&gt;&gt;&gt; getservbyport(21)
'ftp'
&gt;&gt;&gt; getservbyport(512)
'exec'
&gt;&gt;&gt; getservbyport(513)
'login'
&gt;&gt;&gt; getservbyport(514)
'shell'
</code></pre>
<p>As you can see TCP name is used by default.</p>
<p>Generally you probably won't care that much so it's fine to use these functions without specifying protocol name.</p>
<p>If you want accuracy however you will need to specify protocol when translating port name/number and enclose your code in try..except block.</p>
<p><strong>Important note</strong></p>
<p>Results returned by getservbyport and getservbyname are OS dependent.</p>
<p>You should generally get the same names/port numbers regardless of the OS but in some cases one OS might have more services defined than the other.</p>
<ul>
<li>On <em>Linux</em> based systems you can find port definitions in <code>/etc/services</code> file.</li>
<li>If you use <em>Windows</em> the equivalent should be found in <code>%SystemRoot%\system32\drivers\etc\services</code>.</li>
</ul>
<p>If you want something that is OS independent that can be used for quick searching from CLI I recommend looking at <code>whatportis</code> utility, link in <a href="#references">References</a>.</p>
<h2 id="realworldapplications">Real-world applications</h2>
<p>It's definitely useful for humans to be able to check what port name corresponds to what port number. There are however real-world applications where we might want to deploy our newly acquired knowledge.</p>
<p>For instance, look at the below output taken from Arista device:</p>
<pre><code class="language-text">veos1#sh ip access-lists so-many-ports 
IP Access List so-many-ports
        10 deny tcp 10.0.0.0/24 10.0.1.0/24 eq tcpmux
        20 deny tcp 10.0.0.0/24 10.0.1.0/24 eq systat
        30 deny udp 10.0.0.0/24 10.0.1.0/24 eq daytime
        40 deny tcp 10.0.0.0/24 10.0.1.0/24 eq ssh
        50 deny tcp 10.0.0.0/24 10.0.1.0/24 eq telnet
        60 deny udp 10.0.0.0/24 10.0.1.0/24 eq time
        70 deny tcp 10.0.0.0/24 10.0.1.0/24 eq gopher
        80 deny tcp 10.0.0.0/24 10.0.1.0/24 eq acr-nema
        90 deny udp 10.0.0.0/24 10.0.1.0/24 eq qmtp
        100 permit tcp 10.0.0.0/24 10.0.1.0/24 eq ipx
        110 permit tcp 10.0.0.0/24 10.0.1.0/24 eq rpc2portmap
        120 deny tcp 10.0.0.0/24 10.0.1.0/24 eq svrloc
        130 deny udp 10.0.0.0/24 10.0.1.0/24 eq talk
        140 permit tcp 10.0.0.0/24 10.0.1.0/24 eq uucp
        150 deny tcp 10.0.0.0/24 10.0.1.0/24 eq dhcpv6-server
        160 deny tcp 10.0.0.0/24 10.0.1.0/24 eq submission
        170 permit tcp 10.0.0.0/24 10.0.1.0/24 eq ms-sql-m
</code></pre>
<p>It's a made up ACL but one thing stands out, there are no port numbers! Yes, by default Arista will convert port number to a port name both in config and in the output of show commands. In newer versions of EOS you can disable this behaviour but that might not always be allowed.</p>
<p>Real question is, is this a problem? Well, personally most of these port names mean nothing to me so I can't tell if given entry should be there or not. Displaying port names might be a great idea in principle but in my opinion it gets in the way.</p>
<p>Another reason why we might want to stick to port numbers is config abstraction. If I wanted to store this ACL in my Infrastructure as a Code repository I definitely would want to store ports in their numeric format. I don't know what devices I will use in the future and you might want to have multiple system consume this data, so why make it difficult?</p>
<p>Having said that, I wrote a small example program to quickly convert these port names.</p>
<pre><code>from pathlib import Path
from socket import getservbyname

def main():
    with Path(&quot;arista-sh-acl.txt&quot;).open() as fin:
        aces = [line for line in fin.read().split(&quot;\n&quot;) if line]

    aces_clean = []
    for ace in aces:
        units = ace.strip().split(&quot; &quot;)
        prot = units[2]
        port = units[-1]
        if not port.isdigit():
            try:
                port_no = getservbyname(port, prot)
                aces_clean.append(&quot; &quot;.join(units[:-1] + [str(port_no)]))
            except OSError:
                print(f&quot;Couldn't translate port name '{port}' for protocol {prot}&quot;)

    print(&quot;\n&quot;.join(aces_clean))

if __name__ == &quot;__main__&quot;:
    main()
</code></pre>
<p>I saved lines of my ACL in file named <code>arista-sh-acl.txt</code> and ran the program:</p>
<pre><code class="language-shell">(venv) przemek@quark:~/netdev/prot_names$ python acl_no_prot_names.py 
Couldn't translate port name 'qmtp' for protocol udp
Couldn't translate port name 'ipx' for protocol tcp
Couldn't translate port name 'dhcpv6-server' for protocol tcp
10 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 1
20 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 11
30 deny udp 10.0.0.0/24 10.0.1.0/24 eq 13
40 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 22
50 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 23
60 deny udp 10.0.0.0/24 10.0.1.0/24 eq 37
70 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 70
80 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 104
110 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 369
120 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 427
130 deny udp 10.0.0.0/24 10.0.1.0/24 eq 517
140 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 540
160 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 587
170 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 1434
</code></pre>
<p>Promising, but why did it fail on 3 names? Well it turns out that Ubuntu I'm running this on has only one protocol listed next to some of the definitions while Arista translates ports regardless of protocol.</p>
<p>Solution in this case is to just ignore protocol and ask for any definition, updated program below:</p>
<pre><code>from pathlib import Path
from socket import getservbyname

def main():
    with Path(&quot;arista-sh-acl.txt&quot;).open() as fin:
        aces = [line for line in fin.read().split(&quot;\n&quot;) if line]

    aces_clean = []
    for ace in aces:
        units = ace.strip().split(&quot; &quot;)
        prot = units[2]
        port = units[-1]
        if not port.isdigit():
            try:
                port_no = getservbyname(port)
                aces_clean.append(&quot; &quot;.join(units[:-1] + [str(port_no)]))
            except OSError:
                print(f&quot;Couldn't translate port name '{port}' for protocol {prot}&quot;)

    print(&quot;\n&quot;.join(aces_clean))

if __name__ == &quot;__main__&quot;:
    main()
</code></pre>
<p>Let's run it again:</p>
<pre><code class="language-shell">(venv) przemek@quark:~/netdev/prot_names$ python acl_no_prot_names.py 
10 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 1
20 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 11
30 deny udp 10.0.0.0/24 10.0.1.0/24 eq 13
40 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 22
50 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 23
60 deny udp 10.0.0.0/24 10.0.1.0/24 eq 37
70 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 70
80 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 104
90 deny udp 10.0.0.0/24 10.0.1.0/24 eq 209
100 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 213
110 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 369
120 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 427
130 deny udp 10.0.0.0/24 10.0.1.0/24 eq 517
140 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 540
150 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 547
160 deny tcp 10.0.0.0/24 10.0.1.0/24 eq 587
170 permit tcp 10.0.0.0/24 10.0.1.0/24 eq 1434
</code></pre>
<p>Much better now. All names have been translated to numbers!</p>
<p>So there you have it. You should now know how to use Python to translate between port numbers and their names. If you ever need to normalize output from network devices or other systems you'll know how to do it programmatically.</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Python <code>socket</code> library: <a href="https://docs.python.org/3/library/socket.html" target="_blank">https://docs.python.org/3/library/socket.html</a></li>
<li>OS independent utility for port number to name conversion: <a href="https://github.com/ncrocfer/whatportis" target="_blank">https://github.com/ncrocfer/whatportis</a></li>
<li>GitHub repo with source code for this post: <a href="https://github.com/progala/ttl255.com/tree/master/python/port-name-to-number" target="_blank">GitHub repository with code for this post</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Build DSCP to ToS conversion table with Python]]></title><description><![CDATA[In this post we're going to write Python program that generates DSCP to ToS conversion table while avoiding hardcoding values as much as possible. We will then save the final table to csv file with pre-defined column headers.]]></description><link>https://ttl255.com/build-dscp-tos-conversion-table-python/</link><guid isPermaLink="false">5f496ec81e69ff52c5b065e2</guid><category><![CDATA[python]]></category><category><![CDATA[programming]]></category><category><![CDATA[automation]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Mon, 31 Aug 2020 20:51:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="contents">Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#problem-descript">Problem description.</a></li>
<li><a href="#discovery-and-re">Discovery and research phase</a></li>
<li><a href="#plan-of-action">Plan of action</a>
<ul>
<li><a href="#initial-values">Initial values</a></li>
<li><a href="#dscp-to-tos-conv">DSCP to ToS conversion and different number bases</a></li>
<li><a href="#tos-precedence-d">ToS Precedence, Delay, Throughput and Reliability</a></li>
<li><a href="#tos-string-forma">ToS string formats</a></li>
<li><a href="#dscp-classes">DSCP classes</a></li>
<li><a href="#generating-conve">Generating conversion table row</a></li>
<li><a href="#building-final-t">Building final table</a></li>
<li><a href="#writing-table-to">Writing table to CSV file</a></li>
</ul>
</li>
<li><a href="#writing-tests">Writing tests</a>
<ul>
<li><a href="#testing-dscp-cla">Testing <code>dscp_class</code></a></li>
<li><a href="#testing-kth-bit8">Testing <code>kth_bit8_val</code></a></li>
<li><a href="#testing-test-gen">Testing <code>test_gen_table_row</code></a></li>
<li><a href="#running-tests">Running tests</a></li>
</ul>
</li>
<li><a href="#closing-thoughts">Closing thoughts</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/dscp-tos-table" target="_blank">GitHub repository with code for this post</a></li>
</ul>
<p><a name="introduction"></a></p>
<h2 id="introduction">Introduction</h2>
<p>In this post we're going to write Python program that generates DSCP to ToS conversion table while avoiding hardcoding values as much as possible. We will then save the final table to csv file with pre-defined column headers.</p>
<p>I got the idea for this blog article from the <a href="https://twitter.com/nickrusso42518/status/1296852620959776777" target="_blank">tweet</a> posted the other day by Nick Russo. I thought it is an interesting problem to tackle as similar ones pop up all the time during early stages of Network Automation journey. What makes this challenge great is that it requires us to carry out tasks that apply to writing larger programs.</p>
<ul>
<li>We need to understand the problem and possibly do some research.</li>
<li>We have to come up with plan of action.</li>
<li>We need to break down larger tasks into smaller pieces.</li>
<li>We need to implement all of the pieces we identified.</li>
<li>Finally we have to put pieces back together and make sure final product works.</li>
</ul>
<p>Before you continue reading this post I encourage you to try to implement solution to this problem yourself. Programming really is one of those skills that you learn best by doing. Once you've got something in place you can come back and see how I tackled this.</p>
<p>Full disclosure, I posted my hastily put together initial solution on Twitter but I made a lot of tweaks since then. It was lunch time and your first solution will rarely be the final one :) You can find it on <a href="https://gist.github.com/progala/5b47397ffeccbb686ae750613c4f2379" target="_blank">Github</a>, with few revisions due to several mistakes, and see how it evolved into solution I'm presenting here.</p>
<p><a name="problem-descript"></a></p>
<h2 id="problemdescription">Problem description.</h2>
<p>Our goal is to produce DSCP to ToS conversion table, additionally we need to provide multiple representations and bit values for each DSCP and ToS code. The end result should resemble the following CSV:</p>
<p><img src="https://ttl255.com/content/images/2020/08/dscp-tos-tbl.png" alt="dscp-tos-tbl"></p>
<p><a name="discovery-and-re"></a></p>
<h2 id="discoveryandresearchphase">Discovery and research phase</h2>
<p>There is quite a lot going on here but few patterns become apparent quite quickly.</p>
<p>We can see that some columns show the same value with the only change being number base. DSCP and ToS codes are shown in binary, decimal and hexadecimal bases. This is something we should be able to tackle even without understanding much about meaning of DSCP and ToS codes.</p>
<p>Looking at the entries it also appears that ToS = DSCP x 4. But we should not take this for granted, we should do research and refer to documents defining DSCP and ToS. Humans have tendency to see patterns everywhere and this can sometimes make us come up with good solution to a wrong problem.</p>
<p>With that being said I searched for RFC that can help us with understanding what DSCP and ToS are.</p>
<p>Below ToS definition is taken from RFC795:</p>
<pre><code class="language-text">The IP Type of Service has the following fields:

   Bits 0-2:  Precedence.
   Bit    3:  0 = Normal Delay,      1 = Low Delay.
   Bits   4:  0 = Normal Throughput, 1 = High Throughput.
   Bits   5:  0 = Normal Relibility, 1 = High Relibility.
   Bit  6-7:  Reserved for Future Use.

      0     1     2     3     4     5     6     7
   +-----+-----+-----+-----+-----+-----+-----+-----+
   |                 |     |     |     |     |     |
   |   PRECEDENCE    |  D  |  T  |  R  |  0  |  0  |
   |                 |     |     |     |     |     |
   +-----+-----+-----+-----+-----+-----+-----+-----+

   111 - Network Control
   110 - Internetwork Control
   101 - CRITIC/ECP
   100 - Flash Override
   011 - Flash
   010 - Immediate
   001 - Priority
   000 - Routine
</code></pre>
<p>You might have noticed that some items here match elements in our table. Precedence bits 0-2 match binary and decimal ToS Precedence columns as well as the ToS String Format. Bits 3, 4 and 5 match ToS Delay, Throughput and Reliability.</p>
<p>So we figured quite a lot already by noticing that same value is represented in different base number bases and then by looking at one RFC describing ToS.</p>
<p>We now need to figure out DSCP class codepoints (1st column) and understand how does DSCP map to ToS.</p>
<p>And to do that we'll look at more RFCs.</p>
<p>RFC2474 says this about DSCP:</p>
<pre><code class="language-text">   A replacement header field, called the DS field, is defined, which is
   intended to supersede the existing definitions of the IPv4 TOS octet
   [RFC791] and the IPv6 Traffic Class octet [IPv6].

   Six bits of the DS field are used as a codepoint (DSCP) to select the
   PHB a packet experiences at each node.  A two-bit currently unused
   (CU) field is reserved and its definition and interpretation are
   outside the scope of this document.  The value of the CU bits are
   ignored by differentiated services-compliant nodes when determining
   the per-hop behavior to apply to a received packet.

   The DS field structure is presented below:


        0   1   2   3   4   5   6   7
      +---+---+---+---+---+---+---+---+
      |         DSCP          |  CU   |
      +---+---+---+---+---+---+---+---+

        DSCP: differentiated services codepoint
        CU:   currently unused
</code></pre>
<p>Aha! So we now know that ToS uses 8 bits and DSCP uses just first 6 bits of the same byte in the IP header. From that follows that when converting from DSCP to ToS we will have to shift our number by two bits to the left.</p>
<p>But what does that mean? Well, each bit we shift our value by equals to multiplying by 2. In example below we shift binary number 0010 (decimal 2) to the left by 2 bits with resulting number being binary 1000 (decimal 8).</p>
<pre><code class="language-text">bin 0010 - dec 2

shift by 2 bits to the left

bin 1000 - dec 8
</code></pre>
<p>Hopefully now you can see that our initial hunch was correct, ToS = DSCP x 4, but now you also know why, which is much more important.</p>
<p>Great, so we have pretty much all pieces of the puzzle now. Let's find out what are the names of DSCP codepoints.</p>
<p>RFC4594 has this to say on the topic:</p>
<pre><code class="language-text">    ------------------------------------------------------------------
   |   Service     |  DSCP   |    DSCP     |       Application        |
   |  Class Name   |  Name   |    Value    |        Examples          |
   |===============+=========+=============+==========================|
   |Network Control|  CS6    |   110000    | Network routing          |
   |---------------+---------+-------------+--------------------------|
   | Telephony     |   EF    |   101110    | IP Telephony bearer      |
   |---------------+---------+-------------+--------------------------|
   |  Signaling    |  CS5    |   101000    | IP Telephony signaling   |
   |---------------+---------+-------------+--------------------------|
   | Multimedia    |AF41,AF42|100010,100100|   H.323/V2 video         |
   | Conferencing  |  AF43   |   100110    |  conferencing (adaptive) |
   |---------------+---------+-------------+--------------------------|
   |  Real-Time    |  CS4    |   100000    | Video conferencing and   |
   |  Interactive  |         |             | Interactive gaming       |
   |---------------+---------+-------------+--------------------------|
   | Multimedia    |AF31,AF32|011010,011100| Streaming video and      |
   | Streaming     |  AF33   |   011110    |   audio on demand        |
   |---------------+---------+-------------+--------------------------|
   |Broadcast Video|  CS3    |   011000    |Broadcast TV &amp; live events|
   |---------------+---------+-------------+--------------------------|
   | Low-Latency   |AF21,AF22|010010,010100|Client/server transactions|
   |   Data        |  AF23   |   010110    | Web-based ordering       |
   |---------------+---------+-------------+--------------------------|
   |     OAM       |  CS2    |   010000    |         OAM&amp;P            |
   |---------------+---------+-------------+--------------------------|
   |High-Throughput|AF11,AF12|001010,001100|  Store and forward       |
   |    Data       |  AF13   |   001110    |     applications         |
   |---------------+---------+-------------+--------------------------|
   |    Standard   | DF (CS0)|   000000    | Undifferentiated         |
   |               |         |             | applications             |
   |---------------+---------+-------------+--------------------------|
   | Low-Priority  |  CS1    |   001000    | Any flow that has no BW  |
   |     Data      |         |             | assurance                |
    ------------------------------------------------------------------

                Figure 3. DSCP to Service Class Mapping
</code></pre>
<p>Just what we needed! All of the DSCP names and corresponding values in one place.</p>
<p>This has been great but notice that we didn't write a single line of code! And this is how it should be. Often you will feel like jumping right in and getting those Python statements flowing. I would however advise you to spend some time to understand the problem first, you might find out that once you have better grasp of the task it will make it easier to write your code.</p>
<p>With that said, I think we're ready to get our hands dirty :)</p>
<p><a name="plan-of-action"></a></p>
<h2 id="planofaction">Plan of action</h2>
<p>I decided to split this problem by treating value generation for each column as a separate task to implement. I'm also separating generation of values from rendering of the whole table and from writing the table to the CSV file.</p>
<p>Here's my high level plan of action.</p>
<ul>
<li>Decide on what are the initial values.</li>
<li>Generate each of the resulting values from initial values.</li>
<li>Put values together to form table row.</li>
<li>Generate final conversion table.</li>
<li>Write conversion table to CSV file.</li>
</ul>
<p><a name="initial-values"></a></p>
<h3 id="initialvalues">Initial values</h3>
<p>I'm going to start with choosing initial values from which we will derive all of the other items. Natural choice here will be DSCP or ToS codes. You can't go wrong with either but I'll pick DSCP because numbers are smaller and they look more familiar to me.</p>
<p>Looking at DSCP codes in the table from the beginning of the post we have 0, then jump to 8, and then numbers increase by 2 all the way to 40. So that's a nice pattern. Finally we have numbers 46, 48 and 56.</p>
<p>From that we could define a list and type all of the numbers by hand, like so:</p>
<pre><code>dscps_dec = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 46, 48, 56]
</code></pre>
<p>This is a bit verbose though and our original problem asked to use logic where possible, within reason as we want this code to be easy to understand.</p>
<p>Below is what I ended up doing:</p>
<pre><code>dscps_dec = (0, *range(8, 41, 2), 46, 48, 56)
</code></pre>
<p>I created a tuple, with 0, 46, 48, 56 typed manually and the numbers between 8 and 40 are unpacked from sequence returned by range().</p>
<p><a name="dscp-to-tos-conv"></a></p>
<h3 id="dscptotosconversionanddifferentnumberbases">DSCP to ToS conversion and different number bases</h3>
<p>Now that we have our DSCP values we can start looking into generating remaining elements.</p>
<p>First let's get on with DSCP to ToS conversion and representations in different number bases. This should be a good warm up before more involving tasks.</p>
<p>For converting to binary and hexadecimal we'll employ Python's string <code>format()</code> method:</p>
<pre><code class="language-python">&quot;{:06b}&quot;.format(dscp)
&quot;{:#04x}&quot;.format(dscp)
</code></pre>
<p>And a little test:</p>
<pre><code class="language-python">&gt;&gt;&gt; dscp = 22
&gt;&gt;&gt; print(&quot;{:06b}&quot;.format(dscp))
010110
&gt;&gt;&gt; print(&quot;{:#04x}&quot;.format(dscp))
0x16
</code></pre>
<p>That's looking good. Let's look more closely at what's happening here.</p>
<ul>
<li>
<p><code>&quot;{:06b}&quot;</code> - <code>b</code> means take provided number and display it in binary base, <code>6</code> makes field 6 characters wide and <code>0</code> adds 0s in the front if the resulting number has less than 6 digits.</p>
</li>
<li>
<p><code>{:#04x}</code> - <code>x</code> means display number in hexadecimal base, <code>4</code> makes field 4 characters wide, <code>0</code> prepends 0s if needed and <code>#</code> asks for display of base indicator <code>0x</code>.</p>
</li>
</ul>
<p>Not too bad. We've got 3 columns sorted out, now we'll tackle ToS numbers.</p>
<p>We already found out that we need to shift DSCP number by two bits to arrive at ToS value. We could multiply DSCP by 4 but to emphasize our intent here we'll use Python's bitwise operator <code>&lt;&lt;</code>.</p>
<pre><code class="language-python">tos_dec = dscp &lt;&lt; 2
</code></pre>
<p>And some tests:</p>
<pre><code class="language-python">&gt;&gt;&gt; dscp = 16
&gt;&gt;&gt; tos_dec = dscp &lt;&lt; 2
&gt;&gt;&gt; tos_dec
64
&gt;&gt;&gt; dscp = 28
&gt;&gt;&gt; tos_dec = dscp &lt;&lt; 2
&gt;&gt;&gt; tos_dec
112
</code></pre>
<p>With that in hand we can now get bin and hex values, we'll use similar string formatting as we did for DSCP but we'll make binary string field 8 characters wide as ToS values use all 8 bits.</p>
<pre><code class="language-python">&quot;{:#04x}&quot;.format(tos_dec)
&quot;{:08b}&quot;.format(tos_dec)
</code></pre>
<pre><code class="language-python">&gt;&gt;&gt; tos_dec = 112
&gt;&gt;&gt; print(&quot;{:08b}&quot;.format(tos_dec))
01110000
&gt;&gt;&gt; print(&quot;{:#04x}&quot;.format(tos_dec))
0x70
</code></pre>
<p>Perfect, two more columns ticked off.</p>
<p><a name="tos-precedence-d"></a></p>
<h3 id="tosprecedencedelaythroughputandreliability">ToS Precedence, Delay, Throughput and Reliability</h3>
<p>Let's move onto ToS Precedence and Delay, Throughput and Reliability bits. We know that Precedence is encoded by bits 0-2. And we just computed binary number so perhaps we can reuse that?</p>
<p>Here's what I did:</p>
<pre><code class="language-python">tos_bin = &quot;{:08b}&quot;.format(tos_dec)
tos_prec_bin = tos_bin[:3]
</code></pre>
<p>And a little test:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_dec = 112
&gt;&gt;&gt; tos_bin = &quot;{:08b}&quot;.format(tos_dec)
&gt;&gt;&gt; tos_prec_bin = tos_bin[:3]
&gt;&gt;&gt; tos_prec_bin
'011'
</code></pre>
<p>Since we'll be using binary ToS value on it's own and to help us get ToS Precedence value, I assigned result of binary base conversion to a variable. That variable now holds a reference to a string from which we need first 3 digits for ToS Precedence. We can use list slicing here, and assign the slice to a new variable.</p>
<p>We got ToS precedence in binary but we also need it in decimal form. We can take advantage of built-in <code>int()</code> function that can take string representation of number with optional base, and will give use back decimal.</p>
<pre><code class="language-python">int(tos_prec_bin, 2)
</code></pre>
<p>We feed it result from our previous assignment of <code>tos_prec_bin</code>:</p>
<pre><code class="language-python">&gt;&gt;&gt; int(tos_prec_bin, 2)
3
</code></pre>
<p>Hey, it works! This is going quite well so far.</p>
<p>We still got 3 single bits to work out. We can see in RFC795 that we need to check if bit at given position is set or not. We will also need to check bits in 3 different places, so it makes sense to encapsulate this logic in the separate function.</p>
<p>To check if bit is set we should look at bitwise AND which in Python is done with <code>&amp;</code> operator.</p>
<p>Ok, but what do we AND with what? On one side we will have our ToS number but we need to figure the other side.</p>
<p>Looking again at the diagram taken from RFC we see that bits are counted from left to right, starting with bit 0.</p>
<pre><code class="language-text">      0     1     2     3     4     5     6     7
   +-----+-----+-----+-----+-----+-----+-----+-----+
   |                 |     |     |     |     |     |
   |   PRECEDENCE    |  D  |  T  |  R  |  0  |  0  |
   |                 |     |     |     |     |     |
   +-----+-----+-----+-----+-----+-----+-----+-----+
</code></pre>
<p>To check bits 3, 4 and 5 we could use corresponding binary numbers:</p>
<pre><code class="language-text">0b00010000
0b00001000
0b00000100
</code></pre>
<p>If we apply bitwise AND to first binary number <code>0b00010000</code> and whatever value we pass, we will get <code>0b00010000</code> only if passed number has bit 3 set to 1. Otherwise end result will be 0.</p>
<p>Here's an example of how that would work:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_bin = 0b0011_0000
&gt;&gt;&gt; bw_and_res = tos_bin &amp; 0b0001_0000
&gt;&gt;&gt; &quot;{:#010b}&quot;.format(bw_and_res)
'0b00010000'
</code></pre>
<p>Hopefully now you can see that bitwise AND operator can help us check if given bit is set or not.</p>
<p>There's one slight problem though, we want to get back number <strong>1</strong> if bit is set or <strong>0</strong> if it is not. Right now we get back 8-bit binary number. We'll now fix it and encapsulate the logic in the function named <code>kth_bit8_val</code>.</p>
<pre><code class="language-python">def kth_bit8_val(byte, k):
    &quot;&quot;&quot;
    Returns value of k-th bit

    Most Significant Bit, bit-0, on the Left

    :param byte: 8 bit integer to check
    :param k: bit value to return
    :return: 1 if bit is set, 0 if not
    &quot;&quot;&quot;
    return 1 if byte &amp; (0b1000_0000 &gt;&gt; k) else 0
</code></pre>
<p>Our function is essentially one liner that does the following:</p>
<ul>
<li>
<p>Takes provided number in <code>byte</code> argument and k-th bit to check in <code>k</code> argument.</p>
</li>
<li>
<p>Shifts binary number <code>0b10000000</code> to right by k bits with <code>(0b1000_0000 &gt;&gt; k)</code>. This prepares our mask for bitwise AND operation. If we wanted to check bit 3 we'd get <code>0b00010000</code>. Remember we count from 0.</p>
</li>
<li>
<p>With mask in place we can AND our number with the mask <code>(byte &amp; (0b1000_0000 &gt;&gt; k)</code>. Result will be either binary number <strong>0</strong> or binary number equal to our mask if the required bit is set, e.g. <code>0b00010000</code>.</p>
</li>
<li>
<p>Because we don't want the 8-bit binary number but just 1 or 0, we write short <code>if..else</code> statement that returns <strong>0</strong> if binary number is 0 or <strong>1</strong> if it's anything else.</p>
</li>
</ul>
<p>Here's an example of our function in action:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_val = 152
&gt;&gt;&gt; kth_bit8_val(tos_val, 3)
1
&gt;&gt;&gt; kth_bit8_val(tos_val, 4)
1
&gt;&gt;&gt; kth_bit8_val(tos_val, 5)
0
</code></pre>
<p>With our function in place we can now get values for ToS Delay, Throughput and Reliability bits:</p>
<pre><code class="language-python">kth_bit8_val(tos_dec, 3)
kth_bit8_val(tos_dec, 4)
kth_bit8_val(tos_dec, 5)
</code></pre>
<p>Let's try these out:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_dec = 88
&gt;&gt;&gt; kth_bit8_val(tos_dec, 3)
1
&gt;&gt;&gt; kth_bit8_val(tos_dec, 4)
1
&gt;&gt;&gt; kth_bit8_val(tos_dec, 5)
0
&gt;&gt;&gt; f&quot;{tos_dec:08b}&quot;
'01011000'
</code></pre>
<p>As you can see we got correct values for bits 3, 4 and 5. Another task completed :).</p>
<p><a name="tos-string-forma"></a></p>
<h3 id="tosstringformats">ToS string formats</h3>
<p>We now only have 2 tasks left, getting DSCP class and ToS string format. These will require some manual typing but we can still have decent amount of logic.</p>
<p>First we'll get ToS string format out of the way.</p>
<p>If you look at RFC795 excerpt again you should see that ToS strings depend on the value of ToS Precedence field.</p>
<pre><code class="language-text">      0     1     2     3     4     5     6     7
   +-----+-----+-----+-----+-----+-----+-----+-----+
   |                 |     |     |     |     |     |
   |   PRECEDENCE    |  D  |  T  |  R  |  0  |  0  |
   |                 |     |     |     |     |     |
   +-----+-----+-----+-----+-----+-----+-----+-----+

   111 - Network Control
   110 - Internetwork Control
   101 - CRITIC/ECP
   100 - Flash Override
   011 - Flash
   010 - Immediate
   001 - Priority
   000 - Routine
</code></pre>
<p>And we already have that, we used <code>int(tos_prec_bin, 2)</code> before to get value of ToS Precedence field in decimal. Let's assign this to a variable.</p>
<p>Next we'll create a dictionary with keys being all possible ToS numbers and values in dictionary being corresponding strings.</p>
<pre><code class="language-python">tos_prec_dec = int(tos_prec_bin, 2)

TOS_STRING_TBL = {
    0: &quot;Routine&quot;,
    1: &quot;Priority&quot;,
    2: &quot;Immediate&quot;,
    3: &quot;Flash&quot;,
    4: &quot;FlashOverride&quot;,
    5: &quot;Critical&quot;,
    6: &quot;Internetwork Control&quot;,
    7: &quot;Network Control&quot;,
}
</code></pre>
<p>With <code>tos_prec_dec</code> variable in place we can use its value to get from dictionary corresponding string format.</p>
<pre><code class="language-python">TOS_STRING_TBL[tos_prec_dec]
</code></pre>
<p>Example use:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_prec_bin = &quot;110&quot;
&gt;&gt;&gt; tos_prec_dec = int(tos_prec_bin, 2)
&gt;&gt;&gt; TOS_STRING_TBL[tos_prec_dec]
'Internetwork Control'
&gt;&gt;&gt; tos_prec_bin = &quot;010&quot;
&gt;&gt;&gt; tos_prec_dec = int(tos_prec_bin, 2)
&gt;&gt;&gt; TOS_STRING_TBL[tos_prec_dec]
'Immediate'
</code></pre>
<p>That seems to be working fine, awesome! Only one more task left. Getting DSCP class.</p>
<p><a name="dscp-classes"></a></p>
<h3 id="dscpclasses">DSCP classes</h3>
<p>Let's have a look again at what RFC2579 and RFC4594 say about DSCP classes.</p>
<p>RFC2597:</p>
<pre><code class="language-text">
   The RECOMMENDED values of the AF codepoints are as follows: AF11 = '
   001010', AF12 = '001100', AF13 = '001110', AF21 = '010010', AF22 = '
   010100', AF23 = '010110', AF31 = '011010', AF32 = '011100', AF33 = '
   011110', AF41 = '100010', AF42 = '100100', and AF43 = '100110'.  The
   table below summarizes the recommended AF codepoint values.

                        Class 1    Class 2    Class 3    Class 4
                      +----------+----------+----------+----------+
     Low Drop Prec    |  001010  |  010010  |  011010  |  100010  |
     Medium Drop Prec |  001100  |  010100  |  011100  |  100100  |
     High Drop Prec   |  001110  |  010110  |  011110  |  100110  |
                      +----------+----------+----------+----------+
</code></pre>
<p>RFC4594 has this to say on the topic:</p>
<pre><code class="language-text">    ------------------------------------------------------------------
   |   Service     |  DSCP   |    DSCP     |       Application        |
   |  Class Name   |  Name   |    Value    |        Examples          |
   |===============+=========+=============+==========================|
   |Network Control|  CS6    |   110000    | Network routing          |
   |---------------+---------+-------------+--------------------------|
   | Telephony     |   EF    |   101110    | IP Telephony bearer      |
   |---------------+---------+-------------+--------------------------|
   |  Signaling    |  CS5    |   101000    | IP Telephony signaling   |
   |---------------+---------+-------------+--------------------------|
   | Multimedia    |AF41,AF42|100010,100100|   H.323/V2 video         |
   | Conferencing  |  AF43   |   100110    |  conferencing (adaptive) |
   |---------------+---------+-------------+--------------------------|
   |  Real-Time    |  CS4    |   100000    | Video conferencing and   |
   |  Interactive  |         |             | Interactive gaming       |
   |---------------+---------+-------------+--------------------------|
   | Multimedia    |AF31,AF32|011010,011100| Streaming video and      |
   | Streaming     |  AF33   |   011110    |   audio on demand        |
   |---------------+---------+-------------+--------------------------|
   |Broadcast Video|  CS3    |   011000    |Broadcast TV &amp; live events|
   |---------------+---------+-------------+--------------------------|
   | Low-Latency   |AF21,AF22|010010,010100|Client/server transactions|
   |   Data        |  AF23   |   010110    | Web-based ordering       |
   |---------------+---------+-------------+--------------------------|
   |     OAM       |  CS2    |   010000    |         OAM&amp;P            |
   |---------------+---------+-------------+--------------------------|
   |High-Throughput|AF11,AF12|001010,001100|  Store and forward       |
   |    Data       |  AF13   |   001110    |     applications         |
   |---------------+---------+-------------+--------------------------|
   |    Standard   | DF (CS0)|   000000    | Undifferentiated         |
   |               |         |             | applications             |
   |---------------+---------+-------------+--------------------------|
   | Low-Priority  |  CS1    |   001000    | Any flow that has no BW  |
   |     Data      |         |             | assurance                |
    ------------------------------------------------------------------

                Figure 3. DSCP to Service Class Mapping
</code></pre>
<p>So it looks like we can build our logic by looking at first 3 bits and then 2 bits after that. Last bit is always set to <strong>0</strong> so we can ignore it.</p>
<p>Now, if you remember, bits 0-2 correspond to ToS Precedence value, and we already worked that out. Bits 3 and 4 are ToS Delay and Throughput bits, which we know already as well.</p>
<p>It looks like we can pass the values we previously computed to a function and then inside of the function we'll work out the DSCP name.</p>
<p>Below is what I came up with:</p>
<pre><code class="language-python">def dscp_class(bits_0_2, bit_3, bit_4):
    &quot;&quot;&quot;
    Takes values of DSCP bits and computes dscp class

    Bits 0-2 decide major class
    Bit 3-4 decide drop precedence

    :param bits_0_2: int: decimal value of bits 0-2
    :param bit_3: int: value of bit 3
    :param bit_4: int: value of bit 4
    :return: DSCP class name
    &quot;&quot;&quot;
    bits_3_4 = (bit_3 &lt;&lt; 1) + bit_4
    if bits_3_4 == 0:
        dscp_cl = &quot;cs{}&quot;.format(bits_0_2)
    elif (bits_0_2, bits_3_4) == (5, 3):
        dscp_cl = &quot;ef&quot;
    else:
        dscp_cl = &quot;af{}{}&quot;.format(bits_0_2, bits_3_4)

    return dscp_cl
</code></pre>
<p>We take 3 arguments since we will have those available from earlier computations. So we get separately bits 0-2, bit 3 and bit 4.</p>
<p>First we compute decimal value held by bits 3 and 4 as this decides precedence drop.</p>
<pre><code class="language-python">bits_3_4 = (bit_3 &lt;&lt; 1) + bit_4
</code></pre>
<p>With that in hand we can start building <code>if..else</code> logic.</p>
<p>We know that if bits 3 and 4 are equal to <strong>0</strong> we're dealing with <code>cs</code> class. The actual number of the <code>cs</code> class is decided by bits 0-2, and we know value of those. So we can insert that value into the string:</p>
<pre><code class="language-python">if bits_3_4 == 0:
    dscp_cl = &quot;cs{}&quot;.format(bits_0_2)
</code></pre>
<p>Next off we're dealing with an exception, <code>ef</code> class. Here we need to check if bits 0-2 equal to <strong>5</strong> and bits 3-4 equal to <strong>3</strong> as per RFC table:</p>
<pre><code class="language-text">| Telephony     |   EF    |   101110    | IP Telephony bearer      |
</code></pre>
<pre><code class="language-python">elif (bits_0_2, bits_3_4) == (5, 3):
    dscp_cl = &quot;ef&quot;	
</code></pre>
<p>Finally to build string for <code>af</code> class we append value held in bits 0-2 followed by value held in bits 3-4:</p>
<pre><code class="language-python">else:
    dscp_cl = &quot;af{}{}&quot;.format(bits_0_2, bits_3_4)
</code></pre>
<p>Let's see the completed function in action:</p>
<pre><code class="language-python">&gt;&gt;&gt; tos_prec = 0b001
&gt;&gt;&gt; tos_del = 0
&gt;&gt;&gt; tos_thr = 0
&gt;&gt;&gt; dscp_class(tos_prec, tos_del, tos_thr)
'cs1'
&gt;&gt;&gt; tos_prec = 0b010
&gt;&gt;&gt; tos_del = 1
&gt;&gt;&gt; tos_thr = 1
&gt;&gt;&gt; dscp_class(tos_prec, tos_del, tos_thr)
'af23'
&gt;&gt;&gt; tos_prec = 0b101
&gt;&gt;&gt; tos_del = 1
&gt;&gt;&gt; tos_thr = 1
&gt;&gt;&gt; dscp_class(tos_prec, tos_del, tos_thr)
'ef'
</code></pre>
<p>Things are looking good indeed.</p>
<p>And that's it, we've got all of the components in place. Now we need to put them together.</p>
<p><a name="generating-conve"></a></p>
<h3 id="generatingconversiontablerow">Generating conversion table row</h3>
<p>I decided to move building of all of the values for given DSCP code into its own function. It's easier for me to reason about this code, as well as test it later, when it is separated. I named this function <code>dscp_conv_tbl_row</code>:</p>
<pre><code class="language-python">def dscp_conv_tbl_row(dscp):
    &quot;&quot;&quot;
    Generates DSCP to ToS conversion values as well as different representations

    :param dscp: int: decimal DSCP code
    :return: dict with each value assigned to value name
    &quot;&quot;&quot;
    tos_dec = dscp &lt;&lt; 2
    tos_bin = &quot;{:08b}&quot;.format(tos_dec)
    tos_prec_bin = tos_bin[0:3]
    tos_prec_dec = int(tos_prec_bin, 2)
    tos_del_fl = kth_bit8_val(tos_dec, 3)
    tos_thr_fl = kth_bit8_val(tos_dec, 4)
    tos_rel_fl = kth_bit8_val(tos_dec, 5)
    dscp_cl = dscp_class(tos_prec_dec, tos_del_fl, tos_thr_fl)

    tbl_row_vals = (
        dscp_cl,
        &quot;{:06b}&quot;.format(dscp),
        &quot;{:#04x}&quot;.format(dscp),
        dscp,
        tos_dec,
        &quot;{:#04x}&quot;.format(tos_dec),
        tos_bin,
        tos_prec_bin,
        tos_prec_dec,
        tos_del_fl,
        tos_thr_fl,
        tos_rel_fl,
        TOS_STRING_TBL[tos_prec_dec],
    )

    return tbl_row_vals
</code></pre>
<p>We've already seen most of this code, it's just now put together. We're taking DSCP value and computing values for different components that we need in our conversion table. We finally return all of the items in a tuple.</p>
<p>This is how it looks like when we run it:</p>
<pre><code class="language-python">&gt;&gt;&gt; dscp_conv_tbl_row(28)
('af32', '011100', '0x1c', 28, 112, '0x70', '01110000', '011', 3, 1, 0, 0, 'Flash')
</code></pre>
<p>All of the values for one table row. Now we need to write code that computes these tuples for each of DSCP codes we require. Our end goal is to have full table that is saved to CSV. This is a good place to think about how to store the resulting rows.</p>
<p><a name="building-final-t"></a></p>
<h3 id="buildingfinaltable">Building final table</h3>
<p>We know we want to save final product in CSV format so generating one big string is probably not a good idea. Either list of dictionary would work best here.</p>
<p>Python comes with built-in library for working with CSV files, named appropriately enough <code>csv</code>. We can use either of its methods <code>writer()</code> or <code>DictWriter()</code> to write our data to file. I opted for <code>DictWriter()</code> as this allows me to automatically map my data structure onto columns in resulting file. I suggest you try out both and see what works better for you.</p>
<p>Now that I know how I want to write to CSV file I know that my rows will be recorded as list of dictionaries. Time to write function that encapsulates this logic:</p>
<pre><code class="language-python">def gen_dscp_conversion_table():
    &quot;&quot;&quot;
    Generates DSCP to TOS conversion table with rows for selected DSCP codes

    Final table is a list of dictionaries to make writing CSV easier

    :return: list(dict): final DSCP to TOS conversion table
    &quot;&quot;&quot;
    column_names = (
        &quot;DSCP Class&quot;,
        &quot;DSCP (bin)&quot;,
        &quot;DSCP (hex)&quot;,
        &quot;DSCP (dec)&quot;,
        &quot;ToS (dec)&quot;,
        &quot;ToS (hex)&quot;,
        &quot;ToS (bin)&quot;,
        &quot;ToS Prec. (bin)&quot;,
        &quot;ToS Prec. (dec)&quot;,
        &quot;ToS Delay Flag&quot;,
        &quot;ToS Throughput Flag&quot;,
        &quot;ToS Reliability Flag&quot;,
        &quot;TOS String Format&quot;,
    )
    # These are the DSCP codes we're interested in
    dscps_dec = (0, *range(8, 41, 2), 46, 48, 56)

    conv_tbl = [dict(zip(column_names, dscp_conv_tbl_row(dscp))) for dscp in dscps_dec]

    return conv_tbl
</code></pre>
<p>As you can see we first define tuple with column names, these match what will end up in the the CSV file.</p>
<p>Then we create tuple with DSCP values and finally we create list comprehension inside of which we create our dictionaries.</p>
<p>This list comprehension is composed of few elements so I'm going to break it down for you.</p>
<ul>
<li>
<p><code>for dscp in dscps_dec</code> - provides decimal dscp values for which we generate table rows.</p>
</li>
<li>
<p><code>dscp_conv_tbl_row(dscp)</code> - returns tuple with all values for our table row for given dscp.</p>
</li>
<li>
<p><code>zip(column_names, dscp_conv_tbl_row(dscp))</code> - goes over both column names and table row values and pairs 1st item from one with 1st item from second, then it moves to 2nd pair and so on. Result is a sequence of tuples in <code>(column_name, dscp_tbl_row)</code> format.</p>
</li>
<li>
<p><code>dict(zip(column_names, dscp_conv_tbl_row(dscp)))</code> - takes sequence of tuples and turns them into dictionary with 1st element being turned into key and 2nd element becoming value.</p>
</li>
</ul>
<p>The end result of this list comprehension is a list of dictionaries, each dictionary having column names as keys with corresponding values mapped to them.</p>
<p><a name="writing-table-to"></a></p>
<h3 id="writingtabletocsvfile">Writing table to CSV file</h3>
<p>We're almost there. All that's left for us to do is to write fruits of our hard labor into the file.</p>
<pre><code class="language-python">def main():
    dscp_conversion_table = gen_dscp_conversion_table()

    with Path(&quot;dscp_tos_conv_table.csv&quot;).open(mode=&quot;w&quot;, newline=&quot;&quot;) as fout:
        dict_writer = csv.DictWriter(f=fout, fieldnames=dscp_conversion_table[0].keys())

        dict_writer.writeheader()
        dict_writer.writerows(dscp_conversion_table)
</code></pre>
<ul>
<li>
<p>We're generating the full table first. Then we open file we want to write to, we specify write mode and newline is set to <code>&quot;&quot;</code> as required by csv library.</p>
</li>
<li>
<p>Next we create <code>csv.DictWriter()</code> object giving it our file. We set fieldnames argument to keys from the first dictionary in our list. All dictionaries have the same keys so it doesn't matter which one we take.</p>
</li>
<li>
<p>Finally we ask <code>DictWriter</code> to write header to the file, and then we write to the same file all of the entries in one go. We gave <code>DictWriter</code> dictionary keys so it's able to map all of the row values automatically.</p>
</li>
</ul>
<p>And with that we came to an end. Or have we?</p>
<p>Yes and no. We built program generating DSCP to ToS conversion table and we wrote the table to CSV file. But, do we know if this works correctly? Do we know if the values it produces are correct? And even if they are correct now, can we be certain that they will stay correct if we modify the program?</p>
<p>The answer is: we don't really know if any of this is correct. Maybe we checked the wrong bit somewhere or maybe there's a typo in a DSCP class name. Even a program that can fit on two pages of text can introduce a fair number of bugs.</p>
<p><a name="writing-tests"></a></p>
<h2 id="writingtests">Writing tests</h2>
<p>You might already think what I'm thinking: testing. We need testing.</p>
<p>So testing we shall have.</p>
<p>In the course of writing our program we created 4 functions:</p>
<ul>
<li><code>dscp_class</code></li>
<li><code>kth_bit8_val</code></li>
<li><code>dscp_conv_tbl_row</code></li>
<li><code>gen_dscp_conversion_table</code></li>
</ul>
<p>I want to write tests for 3 of these but not <code>gen_dscp_conversion_table</code>. This one mostly relies on values generated by other 3 and I don't feel like we need to have coverage here.</p>
<p><strong>So, how do we test?</strong></p>
<p>We should look at each of these functions and think of values that will give us good confidence in our code being correct. For example <code>dscp_class</code> has <code>if..else</code> statement so it would be a good idea to test it with values that will make code take all possible paths. On top of that we want to add few cases that follow the most common path.</p>
<p>For my tests I'm using <code>pytest</code> and <code>pytest.mark.parametrize()</code> decorator. This allows me to define multiple test cases and pass them as parameters to our test function. Without it we'd have to write multiple <code>assert</code> statements.</p>
<p><a name="testing-dscp-cla"></a></p>
<h3 id="testingdscp_class">Testing <code>dscp_class</code></h3>
<p>For testing <code>dscp_class</code> I chose values that should result in <code>csX</code>, <code>afXY</code> and <code>ef</code> DSCP classes. This accounts for all branches of <code>if..else</code> statement. To make sure our logic is correct I got few different examples for each of the major classes.</p>
<p>Here are the final test values and test code that I came up with:</p>
<pre><code>@pytest.mark.parametrize(
    &quot;bits_0_2, bit_3, bit_4, res_class&quot;,
    [
        (0b000, 0, 0, &quot;cs0&quot;),
        (0b001, 0, 1, &quot;af11&quot;),
        (0b011, 0, 0, &quot;cs3&quot;),
        (0b011, 1, 0, &quot;af32&quot;),
        (0b100, 1, 1, &quot;af43&quot;),
        (0b101, 1, 1, &quot;ef&quot;),
        (0b111, 0, 0, &quot;cs7&quot;),
    ]
)
def test_dscp_class(bits_0_2, bit_3, bit_4, res_class):
    assert dscp_class(bits_0_2, bit_3, bit_4) == res_class
</code></pre>
<p><a name="testing-kth-bit8"></a></p>
<h3 id="testingkth_bit8_val">Testing <code>kth_bit8_val</code></h3>
<p>For <code>kth_bit8_val</code> I decided to choose just few values. Corner case here is checking first and last bits, so we need to make sure these are handled correctly. Also each bit can be either set or unset (0 or 1) so we test if both 0 and 1 bit values are picked up.</p>
<p>Here is our test code:</p>
<pre><code>@pytest.mark.parametrize(
    &quot;byte, k_bit, bit_val&quot;,
    [
        (0b1000_0000, 0, 1),
        (0b1110_1111, 3, 0),
        (0b0100_0110, 5, 1),
        (0b0000_0001, 7, 1),
    ]
)
def test_kth_bit8_val(byte, k_bit, bit_val):
    assert kth_bit8_val(byte, k_bit) == bit_val
</code></pre>
<p><a name="testing-test-gen"></a></p>
<h3 id="testingtest_gen_table_row">Testing <code>test_gen_table_row</code></h3>
<p>Lastly, we have <code>test_gen_table_row</code>. Here I chose values for 7 different ToS strings as well as values corresponding to different DSCP classes. We also want to test if values are returned in the correct order. If my function passes all of these test I will be fairly confident that it is working correctly.</p>
<p>Testing code:</p>
<pre><code>@pytest.mark.parametrize(
    &quot;dscp_code, conv_row_values&quot;,
    [
        (0, (&quot;cs0&quot;, &quot;000000&quot;, &quot;0x00&quot;, 0, 0, &quot;0x00&quot;, &quot;00000000&quot;, &quot;000&quot;, 0, 0, 0, 0, &quot;Routine&quot;)),
        (8, (&quot;cs1&quot;, &quot;001000&quot;, &quot;0x08&quot;, 8, 32, &quot;0x20&quot;, &quot;00100000&quot;, &quot;001&quot;, 1, 0, 0, 0, &quot;Priority&quot;)),
        (12, (&quot;af12&quot;, &quot;001100&quot;, &quot;0x0c&quot;, 12, 48, &quot;0x30&quot;, &quot;00110000&quot;, &quot;001&quot;, 1, 1, 0, 0, &quot;Priority&quot;)),
        (22, (&quot;af23&quot;, &quot;010110&quot;, &quot;0x16&quot;, 22, 88, &quot;0x58&quot;, &quot;01011000&quot;, &quot;010&quot;, 2, 1, 1, 0, &quot;Immediate&quot;)),
        (26, (&quot;af31&quot;, &quot;011010&quot;, &quot;0x1a&quot;, 26, 104, &quot;0x68&quot;, &quot;01101000&quot;, &quot;011&quot;, 3, 0, 1, 0, &quot;Flash&quot;)),
        (40, (&quot;cs5&quot;, &quot;101000&quot;, &quot;0x28&quot;, 40, 160, &quot;0xa0&quot;, &quot;10100000&quot;, &quot;101&quot;, 5, 0, 0, 0, &quot;Critical&quot;)),
        (46, (&quot;ef&quot;, &quot;101110&quot;, &quot;0x2e&quot;, 46, 184, &quot;0xb8&quot;, &quot;10111000&quot;, &quot;101&quot;, 5, 1, 1, 0, &quot;Critical&quot;)),
        (48, (&quot;cs6&quot;, &quot;110000&quot;, &quot;0x30&quot;, 48, 192, &quot;0xc0&quot;, &quot;11000000&quot;, &quot;110&quot;, 6, 0, 0, 0, &quot;Internetwork Control&quot;)),
        (56, (&quot;cs7&quot;, &quot;111000&quot;, &quot;0x38&quot;, 56, 224, &quot;0xe0&quot;, &quot;11100000&quot;, &quot;111&quot;, 7, 0, 0, 0, &quot;Network Control&quot;)),
    ]
)
def test_gen_table_row(dscp_code, conv_row_values):
    assert dscp_conv_tbl_row(dscp_code) == conv_row_values
</code></pre>
<p><a name="running-tests"></a></p>
<h3 id="runningtests">Running tests</h3>
<p>Right, we have test functions, we have test values, time to actually run these tests against our codebase:</p>
<pre><code>F:\projects\dscp_tos_conv
(venv) λ pytest -v
=================== test session starts ====================== 
platform win32 -- Python 3.8.2, pytest-6.0.1, py-1.9.0, \
pluggy-0.13.1 -- f:\projects\dscp_tos_conv\venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: F:\projects\dscp_tos_conv
collected 20 items

test_dscp_tos_table.py::test_dscp_class[0-0-0-cs0] PASSED
[  5%] test_dscp_tos_table.py::test_dscp_class[1-0-1-af11] PASSED
[ 10%] test_dscp_tos_table.py::test_dscp_class[3-0-0-cs3] PASSED
[ 15%] test_dscp_tos_table.py::test_dscp_class[3-1-0-af32] PASSED
[ 20%] test_dscp_tos_table.py::test_dscp_class[4-1-1-af43] PASSED
[ 25%] test_dscp_tos_table.py::test_dscp_class[5-1-1-ef] PASSED
[ 30%] test_dscp_tos_table.py::test_dscp_class[7-0-0-cs7] PASSED
[ 35%] test_dscp_tos_table.py::test_kth_bit8_val[128-0-1] PASSED
[ 40%] test_dscp_tos_table.py::test_kth_bit8_val[239-3-0] PASSED
[ 45%] test_dscp_tos_table.py::test_kth_bit8_val[70-5-1] PASSED
[ 50%] test_dscp_tos_table.py::test_kth_bit8_val[1-7-1] PASSED
[ 55%] test_dscp_tos_table.py::test_gen_table_row[0-conv_row_values0] PASSED
[ 60%] test_dscp_tos_table.py::test_gen_table_row[8-conv_row_values1] PASSED
[ 65%] test_dscp_tos_table.py::test_gen_table_row[12-conv_row_values2] PASSED
[ 70%] test_dscp_tos_table.py::test_gen_table_row[22-conv_row_values3] PASSED
[ 75%] test_dscp_tos_table.py::test_gen_table_row[26-conv_row_values4] PASSED
[ 80%] test_dscp_tos_table.py::test_gen_table_row[40-conv_row_values5] PASSED
[ 85%] test_dscp_tos_table.py::test_gen_table_row[46-conv_row_values6] PASSED
[ 90%] test_dscp_tos_table.py::test_gen_table_row[48-conv_row_values7] PASSED
[ 95%] test_dscp_tos_table.py::test_gen_table_row[56-conv_row_values8] PASSED
[100%]

=================== 20 passed in 0.07s ================================

</code></pre>
<p>Awesome, all of the tests passed!</p>
<p>You might think this looks great but I did make some mistakes in my code before showing you the final run, that's the whole idea of testing, to give you confidence that your code works they way you expect it to. And when it doesn't you go back and fix it :)</p>
<p>Without tests you could miss bugs lurking somewhere so it really is a good idea to get used to creating them alongside your main program. Hopefully I showed you here that no code is too small to have a few tests thrown in for good measure, the future you will be thankful!</p>
<p><a name="closing-thoughts"></a></p>
<h2 id="closingthoughts">Closing thoughts</h2>
<p>I had fun working on this challenge. It really is a good use case for problem decomposition, learning string formatting and bit-wise operators. And despite not looking like a tough one at first it did require me to dig a bit deeper and make sure that I understand the problem I'm trying to solve.</p>
<p>Finally after making few silly mistakes during refactoring I was reminded that testing is rarely, if ever, optional. You can get away with not writing tests for long time but sooner or later you'll wish you'd learned it earlier!</p>
<p>I hope you learned something new and enjoyed this post as much as I did writing it :)</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Python <code>csv</code> library: <a href="https://docs.python.org/3/library/csv.html" target="_blank">https://docs.python.org/3/library/csv.html</a></li>
<li>Pytest parametrize: <a href="https://docs.pytest.org/en/stable/parametrize.html" target="_blank">https://docs.pytest.org/en/stable/parametrize.html</a></li>
<li>Python string formatting guide: <a href="https://realpython.com/python-formatted-output/" target="_blank">https://realpython.com/python-formatted-output/</a></li>
<li>Service Mappings: <a href="https://tools.ietf.org/html/rfc795" target="_blank">https://tools.ietf.org/html/rfc795</a></li>
<li>Definition of DS Field: <a href="https://tools.ietf.org/html/rfc2474" target="_blank">https://tools.ietf.org/html/rfc2474</a></li>
<li>Assured Forwarding PHB Group: <a href="https://tools.ietf.org/html/rfc2597" target="_blank">https://tools.ietf.org/html/rfc2597</a></li>
<li>Configuration Guidelines for DiffServ: <a href="https://tools.ietf.org/html/rfc4594" target="_blank">https://tools.ietf.org/html/rfc4594</a></li>
<li>GitHub repo with source code for this post: <a href="https://github.com/progala/dscp-tos-table" target="_blank">GitHub repository with code for this post</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Vrnetlab - Run virtual routers in Docker containers]]></title><description><![CDATA[In this post we learn how to use Vrnetlab to run virtual network topologies in Docker containers.]]></description><link>https://ttl255.com/vrnetlab-run-virtual-routers-in-docker-containers/</link><guid isPermaLink="false">5f3c2f7b1e69ff52c5b065d4</guid><category><![CDATA[automation]]></category><category><![CDATA[Docker]]></category><category><![CDATA[tools]]></category><category><![CDATA[Vrnetlab]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Wed, 19 Aug 2020 21:20:26 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>It’s time to have a look at some Network Automation tools. Today I want to introduce you to Vrnetlab, great piece of software that allows you to run virtual routers inside Docker containers. We’ll talk about what Vrnetlab does and what are its selling points. Then we’ll see how to bring up lab devices by hand and how to use them.</p>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#vrnetlab-overvie">Vrnetlab overview</a>
<ul>
<li><a href="#why-vrnetlab">Why Vrnetlab?</a></li>
<li><a href="#vrnetlab-interna">Vrnetlab internals</a></li>
<li><a href="#few-words-on-doc">Few words on Docker</a></li>
</ul>
</li>
<li><a href="#installing-vrnet">Installing Vrnetlab and its dependencies</a>
<ul>
<li><a href="#installing-kvm">Installing KVM</a></li>
<li><a href="#installing-docke">Installing Docker engine</a></li>
<li><a href="#clone-vrnetlab-r">Clone Vrnetlab repository</a></li>
</ul>
</li>
<li><a href="#building-images">Building images with virtual devices</a></li>
<li><a href="#launching-virtua">Launching virtual devices</a></li>
<li><a href="#helper-bash-func">Helper bash functions</a></li>
<li><a href="#accessing-device">Accessing devices</a></li>
<li><a href="#connecting-devic">Connecting devices together</a></li>
<li><a href="#bring-up-pre-def">Bring up pre-defined topology with Docker Compose</a>
<ul>
<li><a href="#accessing-lab-fr">Accessing lab from outside</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/vrnetlab/vrnetlab-router-in-docker" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="vrnetlab-overvie"></a></p>
<h2 id="vrnetlaboverview">Vrnetlab overview</h2>
<p>Vrnetlab provides convenient way of building virtualized network environments by leveraging existing Docker ecosystem.</p>
<p>This means that you can take image of virtual appliance provided by the vendor and use Vrnetlab to create containers for it. The selling point here is that the whole tool-chain was created with automation in mind, that is you can build your network automation CI pipeline on top of Vrnetlab and no human is needed to spin up the environment, and run desired tests or validations.</p>
<p>At the same time, you can still access the virtual devices via telnet or ssh, which is handy for quick labbing. I do that often when I want to make sure I got the CLI syntax right.</p>
<p>It's worth mentioning that Vrnetlab is heavily used at Deutsche Telekom where it was developed by Kristian Larsson <a href="https://twitter.com/plajjan" target="_blank">@plajjan</a>. So it's not just a tiny pet project, it's very much a grown up tool.</p>
<p><a name="why-vrnetlab"></a></p>
<h3 id="whyvrnetlab">Why Vrnetlab?</h3>
<p>Why use Vrnetlab and not GNS3, EVE-NG or some other emulation software? The way I see it, other tools put emphasis on GUI and interactive labbing whereas Vrnetlab is very lightweight with focus on use in automated pipelines. With exception of Docker, which most of the CI/CD systems already use heavily, there’s no actual software to install, it’s mostly container building scripts and helper tools.</p>
<p><a name="vrnetlab-interna"></a></p>
<h3 id="vrnetlabinternals">Vrnetlab internals</h3>
<p>A lot of heavy-lifting in Vrnetlab is done by tools it provides for building Docker container images with virtual appliances, that you get from networking vendors. Inside of final Docker container we have qemu/KVM duo responsible for running virtualized appliances.</p>
<p>Now, if you wanted to run these appliances individually you’d have to understand what’s  required for turning them up, what resources they need and how can their virtual interfaces be exposed to the external world, to mention just a few things. And if you ever tried to do it yourself you know that all of this can vary wildly not only from vendor to vendor, but even between virtual images from the same vendor.</p>
<p>With Vrnetlab, you get build and bootstrap scripts for each of the supported appliances. These scripts are responsible for setting things like default credentials, management IPs, uploading licenses or even packaging multiple images in some cases. You don't have to worry about any of that anymore, Vrnetlab takes care of it all.</p>
<p>You can think of it as having an abstraction layer on top of various virtual appliances. Once you built your containers you will be able to access and manage them in standardized fashion regardless of the image they contain.</p>
<p>In other words, Vrnetlab is a perfect match for the world of Network Automation!</p>
<p>Vrnetlab currently supports virtual devices from the following vendors:</p>
<ul>
<li>Arista</li>
<li>Cisco</li>
<li>Juniper</li>
<li>Nokia</li>
<li>Mikrotik</li>
<li>OpenWrt (Open Source OS)</li>
</ul>
<p>You can check GitHub repository to see up to date list of available docker image builders.</p>
<p><a name="few-words-on-doc"></a></p>
<h3 id="fewwordsondocker">Few words on Docker</h3>
<p>If you haven't used Docker much, don't worry, Docker is used here mostly as packaging system. This allows us to leverage an existing tooling ecosystem. Many CI systems have support for bringing up, running tests within, and tearing down applications packaged as Docker images. You can also use container orchestration systems, like Kubernetes, to launch and control your virtual appliances.</p>
<p>You don’t actually need to know much about Docker apart from few commands which I’ll show you. Also, building steps and examples are well documented in the Vrnetlab repository.</p>
<p><a name="installing-vrnet"></a></p>
<h2 id="installingvrnetlabanditsdependencies">Installing Vrnetlab and its dependencies</h2>
<p>Vrnetlab has a few prerequisites and there are some things to look out for so I’m including full installation procedure below.</p>
<p>For reference, this is the setup I used for this blog post:</p>
<ul>
<li>VMware Workstation Pro running under Windows</li>
<li>My virtual machine has virtualization extension enabled</li>
<li>Guest operating system is fresh install of Ubuntu 20.04.1 LTS</li>
</ul>
<p><strong>Note</strong>: If you’re running Vrnetlab in virtual environment you need to make sure that your environment supports nested virtualization. This is needed by KVM and Vrnetlab won’t work correctly without this.</p>
<p><a name="installing-kvm"></a></p>
<h3 id="installingkvm">Installing KVM</h3>
<ul>
<li>First make sure that virtualization is enabled, 0 means no hw virtualization support, 1 or more is good.</li>
</ul>
<pre><code class="language-shell">przemek@quark ~$ egrep --count &quot;vmx|smv&quot; /proc/cpuinfo
8
</code></pre>
<ul>
<li>Install KVM requisite packages.</li>
</ul>
<pre><code class="language-shell">przemek@quark ~$ sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
</code></pre>
<ul>
<li>Add your user to groups ‘kvm’ and ‘libvirt’. Logout and login after for changes to take effect.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ sudo adduser `id -un` kvm
Adding user `przemek' to group `kvm' ...
Adding user przemek to group kvm
Done.
przemek@quark:~$ sudo adduser `id -un` libvirt
Adding user `przemek' to group `libvirt' ...
Adding user przemek to group libvirt
Done.
</code></pre>
<ul>
<li>Confirm KVM is installed and operational. Empty output is fine; if something went wrong you’ll get errors.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ virsh list --all
 Id   Name   State
--------------------
</code></pre>
<p>And that’s it for KVM.</p>
<p><a name="installing-docke"></a></p>
<h3 id="installingdockerengine">Installing Docker engine</h3>
<ul>
<li>First we need to update package index and get apt ready to install HTTP repos.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ sudo apt-get update

przemek@quark:~$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    Software-properties-common
</code></pre>
<ul>
<li>Next add GPG key for Docker to apt and verify the key matches fingerprint <code>9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88</code></li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

przemek@quark:~$ sudo apt-key fingerprint 0EBFCD88

pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) &lt;docker@docker.com&gt;
sub   rsa4096 2017-02-22 [S]
</code></pre>
<ul>
<li>Add stable Docker repository to apt.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ sudo add-apt-repository \
   &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable&quot;
</code></pre>
<ul>
<li>Finally update the package index and install Docker Engine and containerd.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ sudo apt-get update
przemek@quark:~$ sudo apt-get install docker-ce docker-ce-cli containerd.io
</code></pre>
<ul>
<li>Run basic container to confirm installation was successful.</li>
</ul>
<pre><code class="language-shell">sudo docker run hello-world
</code></pre>
<ul>
<li>Optionally, add your username to the <code>docker</code> group if you want to use Docker as a non-root user. I tend to do in lab environment but this might not be appropriate for your production environment, so check with your security team before doing this in prod.</li>
</ul>
<pre><code class="language-shell">przemek@quark:~$ sudo usermod -aG docker `id -un`
</code></pre>
<p>Docker is now installed.</p>
<p><a name="clone-vrnetlab-r"></a></p>
<h3 id="clonevrnetlabrepository">Clone Vrnetlab repository</h3>
<p>The quickest way to get Vrnetlab is to clone it from its repository:</p>
<pre><code class="language-shell">przemek@quark:~/netdev$ git clone https://github.com/plajjan/vrnetlab.git
Cloning into 'vrnetlab'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 2558 (delta 3), reused 5 (delta 0), pack-reused 2545
Receiving objects: 100% (2558/2558), 507.67 KiB | 227.00 KiB/s, done.
Resolving deltas: 100% (1567/1567), done.
przemek@quark:~/netdev$ ls
Vrnetlab
</code></pre>
<p>And that’s it! Vrnetlab and all of its dependencies are installed and we’re ready to roll!</p>
<p><a name="building-images"></a></p>
<h2 id="buildingimageswithvirtualdevices">Building images with virtual devices</h2>
<p>I mentioned previously that Vrnetlab provides build scripts for plenty of virtual routers from different vendors. What we don’t however get is the actual images.</p>
<p>Licenses that come with appliances don’t allow repackaging and distribution from sources other than the official channels. We will have to procure images ourselves.</p>
<p>If you don’t have any images handy and you just want to follow this post, you can register for free account with Arista and download image from the below link:</p>
<p><a href="https://www.arista.com/en/support/software-download">https://www.arista.com/en/support/software-download</a></p>
<p>You will need two images:</p>
<ul>
<li>AbootAboot-veos-serial-8.0.0.iso</li>
<li>vEOS-lab-4.18.10M.vmdk</li>
</ul>
<p>They are highlighted in green on the below screenshot:</p>
<p><img src="https://ttl255.com/content/images/2020/08/veos-img-4_18sm-1.png" alt="veos-img-4_18sm-1"></p>
<p>Once you downloaded the images, you need to copy them to <code>veos</code> directory inside of <code>vrnetlab</code> directory. The end result should match the below output:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab/veos$ ls
Aboot-veos-serial-8.0.0.iso  Makefile   vEOS-lab-4.18.10M.vmdk
docker                       README.md
</code></pre>
<p>With files in place we’re ready to kick off Docker image build by running <code>make</code> command inside of the directory:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab/veos$ make
Makefile:18: warning: overriding recipe for target 'docker-pre-build'
../makefile.include:18: warning: ignoring old recipe for target 'docker-pre-build'
for IMAGE in vEOS-lab-4.18.10M.vmdk; do \
	echo &quot;Making $IMAGE&quot;; \
	make IMAGE=$IMAGE docker-build; \
done
Making vEOS-lab-4.18.10M.vmdk

...( cut for brevity )

 ---&gt; Running in f755177783db
Removing intermediate container f755177783db
 ---&gt; 71673f34bae9
Successfully built 71673f34bae9
Successfully tagged vrnetlab/vr-veos:4.18.10M
make[1]: Leaving directory '/home/przemek/netdev/vrnetlab/veos'

</code></pre>
<p>If everything worked correctly you should now have a new Docker image available locally. You can confirm that by running <code>docker images</code> command.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab/veos$ docker images vrnetlab/vr-veos
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
vrnetlab/vr-veos    4.18.10M            71673f34bae9        32 minutes ago      894MB
</code></pre>
<p>You can optionally rename the image, to make the name shorter, or if you want to push it to your local docker registry.</p>
<pre><code class="language-shell">przemek@quark:~$ docker tag vrnetlab/vr-veos:4.18.10M veos:4.18.10M
</code></pre>
<p>Now our image has 2 different names:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/repos/nginx-proxy$ docker images | grep 4.18.10
vrnetlab/vr-veos      4.18.10M            71673f34bae9        34 minutes ago      894MB
veos                  4.18.10M            71673f34bae9        34 minutes ago      894MB
</code></pre>
<p>You can safely delete the default name if you wish so:</p>
<pre><code class="language-shell">przemek@quark:~/netdev$ docker rmi vrnetlab/vr-veos:4.18.10M
Untagged: vrnetlab/vr-veos:4.18.10M
przemek@quark:~/netdev$ docker images | grep 4.18.10
veos                  4.18.10M            71673f34bae9        53 minutes ago      894MB
</code></pre>
<p><a name="launching-virtua"></a></p>
<h2 id="launchingvirtualdevices">Launching virtual devices</h2>
<p>With that, all pieces are in place for us to run our first virtual router in Docker!</p>
<p>Run the below command to start container:</p>
<p><code>docker run -d --name veos1 --privileged veos:4.18.10M</code></p>
<p>This tells docker to start new container in the background using image we built. Argument <code>--privileged</code> is required by KVM, argument <code>--name</code> gives our chosen name to container, and the last argument <code>veos:4.18.10M</code> is the name of the image.</p>
<p>Here’s the command in action:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab$ docker run -d --name veos1 --privileged veos:4.18.10M
c62299c4785f5f6489e346ea18e88f584b18f6f91e50b7c0490ef4752e926dc8

rzemek@quark:~/netdev/vrnetlab$ docker ps | grep veos1
c62299c4785f        veos:4.18.10M             &quot;/launch.py&quot;             55 seconds ago      
Up 54 seconds (health: starting)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp          veos1
</code></pre>
<p>If all worked you should immediately get back Container ID. After that we can check if container is running with commands <code>docker ps</code>.</p>
<p>In our case container is up but it’s not ready yet, this is because we have <code>(health: starting)</code> in the output of <code>docker ps</code> command. Vrnetlab builds Docker images with a healthcheck script which allows us to check if container is fully up and ready for action.</p>
<p>With vEOS it usually takes around 3 minutes for the container to be fully up. This time will most likely be different for other images.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab$ docker ps | grep veos1
c62299c4785f        veos:4.18.10M             &quot;/launch.py&quot;             3 minutes ago       Up 3 minutes (healthy)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp          veos1
</code></pre>
<p>Here we go, we can see that our virtual device is fully up and ready now.</p>
<p>So what’s next? We should probably log into the device and play around, right?</p>
<p>Wait, but how do we do that? Read on to find out!</p>
<p><a name="helper-bash-func"></a></p>
<h2 id="helperbashfunctions">Helper bash functions</h2>
<p>Vrnetlab comes with few useful functions defined in file <code>vrnetlab.sh</code>. If you use <code>bash</code> as your shell you can load these with command <code>. vrnetlab.sh</code> or <code>source vrnetlab.sh</code>. If that doesn't work you'll have to consult manual for your shell.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab$ ls | grep vrnet
vrnetlab.sh

przemek@quark:~/netdev/vrnetlab$ . vrnetlab.sh
</code></pre>
<p>Once your shell loaded the functions you should have access to the below commands.</p>
<ul>
<li>
<p><code>vrcons CONTAINER_NAME</code> - Connects to the console of your virtual device.</p>
<pre><code class="language-shell">przemek@quark:~$ vrcons veos1
Trying 172.17.0.4...
Connected to 172.17.0.4.
Escape character is '^]'.

localhost#
</code></pre>
</li>
<li>
<p><code>vrssh CONTAINER_NAME [USERNAME]</code> - Login to your device via ssh. If USERNAME is not provided default <code>vrnetlab</code> username is used with default password being <code>VR-netlab9</code>.</p>
<p>Default user:</p>
<pre><code class="language-shell">przemek@quark:~$ vrssh veos1
Last login: Wed Aug 19 19:19:07 2020 from 10.0.0.2
localhost&gt;who
    Line      User           Host(s)       Idle        Location 
   1 con 0    admin          idle          00:04:25    -        
*  2 vty 7    vrnetlab       idle          00:00:05    10.0.0.2 
</code></pre>
<p>Custom username:</p>
<pre><code class="language-shell">przemek@quark:~$ vrssh veos1 przemek
Password: 
localhost&gt;
</code></pre>
</li>
<li>
<p><code>vrbridge DEVICE1 DEV1_PORT DEVICE2 DEV2_PORT</code> - Creates connection between interface DEV1_PORT on DEVICE1 and interface DEV2_PORT on DEVICE2.</p>
<pre><code class="language-shell">przemek@quark:~$ vrbridge veos1 3 veos2 3
ae8e6807e3817eaa05429a9357ffb887a27ddf844f06be3b293ca92a6e9d4103
przemek@quark:~$ docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS                    PORTS                                                                                NAMES
ae8e6807e381        vr-xcon                   &quot;/xcon.py --p2p veos…&quot;   3 seconds ago       Up 2 seconds                                                                                                   bridge-veos1-3-veos2-3
</code></pre>
</li>
<li>
<p><code>vr_mgmt_ip CONTAINER_NAME</code> - Tells you what IP was assigned to CONTAINER_NAME.</p>
<pre><code class="language-shell">przemek@quark:~$ vr_mgmt_ip veos1
172.17.0.4
</code></pre>
</li>
</ul>
<p>All of these are very helpful but we’ll mostly be using <code>vrcons</code> and <code>vrbridge</code> in this post.</p>
<p><a name="accessing-device"></a></p>
<h2 id="accessingdevices">Accessing devices</h2>
<p>Now that we have helper functions loaded into our shell we can access the device.</p>
<p>We’ll use <code>vrcons</code> command to connect to the console of our virtual router:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab$ vrcons veos1
Trying 172.17.0.4...
Connected to 172.17.0.4.
Escape character is '^]'.

localhost#sh ver
Arista vEOS
Hardware version:    
Serial number:       
System MAC address:  5254.0094.eeff

Software image version: 4.18.10M
Architecture:           i386
Internal build version: 4.18.10M-10003124.41810M
Internal build ID:      f39d7d34-f2ee-45c5-95a3-c9bd73696ee3

Uptime:                 34 minutes
Total memory:           1893312 kB
Free memory:            854036 kB

localhost#

</code></pre>
<p>Well, well, look at that, we’re in and everything looks to be in order!</p>
<p>Personally I often do just that, launch a container, test a few commands and I’m out. I just love how quickly you can spin up a test device and then get rid of it after you’re done. There’s hardly anything involved in the setup. You just need to wait a few minutes and it’s all ready for you.</p>
<p>Great, we built container with virtual vEOS, we brought it up and managed to run some commands. But while that can be great for quickly labbing up some commands, we want more, we want to connect virtual devices together. After all that’s what networking is about right? Connect all the things!</p>
<p><a name="connecting-devic"></a></p>
<h2 id="connectingdevicestogether">Connecting devices together</h2>
<p>Before we do anything else, let’s get second container up so that it’s ready when we need it:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab$ docker run -d --name veos2 --privileged veos:4.18.10M
fdd6acadacfea340be16fb47757913d3d9cb803f72bdad6fe7276a6b626c325a
</code></pre>
<p>To create connections between devices we need to build special Docker image, called <code>vr-xcon</code>. Containers using this image will provide connectivity between our virtual routes.</p>
<p>To build this image enter directory with vrnetlab repo and then navigate into <code>vr-xcon</code> directory. Once you’re in the directory type <code>make</code>. Vrnetlab scripts will do the rest.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/vrnetlab/vr-xcon$ make
docker build --build-arg http_proxy= --build-arg https_proxy= -t vrnetlab/vr-xcon .
Sending build context to Docker daemon   34.3kB
Step 1/6 : FROM debian:stretch
 ---&gt; 5df937d2ac6c
Step 2/6 : MAINTAINER Kristian Larsson &lt;kristian@spritelink.net&gt;
 ---&gt; Using cache
 ---&gt; a5bf654bbf7c
Step 3/6 : ENV DEBIAN_FRONTEND=noninteractive
 ---&gt; Using cache
 ---&gt; 6d2b8962f440
Step 4/6 : RUN apt-get update -qy  &amp;&amp; apt-get upgrade -qy  &amp;&amp; apt-get install -y     bridge-utils     iproute2     python3-ipy     tcpdump     telnet  &amp;&amp; rm -rf /var/lib/apt/lists/*
 ---&gt; Running in d3df4c947d25

... (cut for brevity)

Removing intermediate container d3df4c947d25
 ---&gt; fbc93d1624f4
Step 5/6 : ADD xcon.py /
 ---&gt; 4cb6b8a3c55a
Step 6/6 : ENTRYPOINT [&quot;/xcon.py&quot;]
 ---&gt; Running in 186f13b11bd8
Removing intermediate container 186f13b11bd8
 ---&gt; bd1542effee7
Successfully built bd1542effee7
Successfully tagged vrnetlab/vr-xcon:latest
</code></pre>
<p>And to confirm that image is now available:</p>
<pre><code class="language-shell">przemek@quark:~$ docker images | grep xcon
vrnetlab/vr-xcon      latest              bd1542effee7        21 hours ago        152MB
</code></pre>
<p>We just need to change its name to just <code>vr-xcon</code> to make it work with Vrnetlab scripts.</p>
<pre><code class="language-shell">przemek@quark:~$ docker tag vrnetlab/vr-xcon:latest vr-xcon
przemek@quark:~$ docker images | grep &quot;^vr-xcon&quot;
vr-xcon               latest              bd1542effee7        22 hours ago        152MB
</code></pre>
<p>Perfect, our <code>veos2</code> container should be up too now:</p>
<pre><code class="language-shell">przemek@quark:~$ docker ps | grep veos2
7841eb2dd336        veos:4.18.10M             &quot;/launch.py&quot;             4 minutes ago       Up 4 minutes (healthy)    22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp          veos2
</code></pre>
<p>Time to connect <code>veos1</code> to <code>veos2</code>, let's do it the hard way first and then I'll show you the easy way.</p>
<pre><code class="language-shell">przemek@quark:~$ docker run -d --name vr-xcon --link veos1 --link veos2 vr-xcon --p2p veos1/2--veos2/2
bfe3f53cb14d6d6c6d2800a7542f8a0bad6c7347c16a313f9812ed86ef808c3f
</code></pre>
<p>Below is the breakdown of the command.</p>
<p><code>docker run -d</code> - This tells docker to run container in the background.</p>
<p><code>--name vr-xcon</code> - We want our container to be named <code>vr-xcon</code>, you can call it something different if you want.</p>
<p><code>--link veos1 --links veos2</code> - Here we tell Docker to connect <code>vr-xcon</code> to <code>veos1</code> and <code>veos2</code> containers. This allows them to discover and talk to each other.</p>
<p><code>vr-xcon</code> - Second reference to <code>vr-xcon</code> is the name of the image to run.</p>
<p><code>--p2p veos1/2--veos2/2</code> - Finally we have arguments that are passed to <code>vr-xcon</code> container. Here we ask for point-to-point connection between <code>veos1</code> port 2 and <code>veos2</code> also port 2. Port 1 maps to Management port so port 2 will be Ethernet1 inside the virtual router.</p>
<p>Hopefully you can now see how it all ties together. To confirm container is up and running we'll run <code>docker ps</code> and we'll check container logs.</p>
<pre><code class="language-shell">przemek@quark:~$ docker ps | grep xcon
bfe3f53cb14d        vr-xcon                   &quot;/xcon.py --p2p veos…&quot;   9 minutes ago       Up 9 minutes                                                                                            vr-xcon
przemek@quark:~$ docker logs vr-xcon
przemek@quark:~$
</code></pre>
<p>Looks promising, container is up and logs are empty, so no errors reported.</p>
<p>But what is that easy way you ask? Remember the <code>vrbridge</code> helper function? We could use that instead:</p>
<pre><code class="language-shell">przemek@quark:~$ vrbridge veos1 2 veos2 2
83557c0406995b17be306cbc365a9c911696221b0542ba2fbb7cdeaf9b442426
przemek@quark:~$ docker ps | grep bridge
83557c040699        vr-xcon                   &quot;/xcon.py --p2p veos…&quot;   38 seconds ago      Up 38 seconds                                                                                                  bridge-veos1-2-veos2-2
</code></pre>
<p>So that works as well. But you can only use it to create one link between two devices. I use it for quickly creating single links. For more involving jobs I use <code>vr-xcon</code> directly, that's why I wanted to show you exactly how it work.</p>
<p>In any case, we got link in place and we're ready to log into devices and confirm if our newly created connection is up.</p>
<p>We'll configure hostnames so that output from LLDP check includes them. We'll then configure IPs and try pinging across.</p>
<pre><code class="language-shell">przemek@quark:~$ vrcons veos1
Trying 172.17.0.4...
Connected to 172.17.0.4.
Escape character is '^]'.

localhost#conf t
localhost(config)#host veos1
veos1(config)#int eth1
veos1(config-if-Et1)#no switch
veos1(config-if-Et1)#ip add 10.10.0.0/31
veos1(config-if-Et1)#end
veos1#sh lldp ne
Last table change time   : 0:00:17 ago
Number of table inserts  : 1
Number of table deletes  : 0
Number of table drops    : 0
Number of table age-outs : 0

Port       Neighbor Device ID               Neighbor Port ID           TTL
Et1        veos2                            Ethernet1                  120
veos1#ping 10.10.0.1
PING 10.10.0.1 (10.10.0.1) 72(100) bytes of data.
80 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=788 ms
80 bytes from 10.10.0.1: icmp_seq=2 ttl=64 time=168 ms
80 bytes from 10.10.0.1: icmp_seq=3 ttl=64 time=176 ms
80 bytes from 10.10.0.1: icmp_seq=4 ttl=64 time=108 ms
80 bytes from 10.10.0.1: icmp_seq=5 ttl=64 time=28.0 ms

--- 10.10.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 2728ms
rtt min/avg/max/mdev = 28.002/253.615/788.050/272.424 ms, ipg/ewma 682.042/508.167 ms
veos1#
</code></pre>
<pre><code class="language-shell">przemek@quark:~$ vrcons veos2
Trying 172.17.0.5...
Connected to 172.17.0.5.
Escape character is '^]'.

localhost#conf t
localhost(config)#host veos2
veos2(config)#int e1
veos2(config-if-Et1)#no switch
veos2(config-if-Et1)#ip add 10.10.0.1/31
veos2(config-if-Et1)#end
veos2#sh lldp ne
Last table change time   : 0:01:37 ago
Number of table inserts  : 1
Number of table deletes  : 0
Number of table drops    : 0
Number of table age-outs : 0

Port       Neighbor Device ID               Neighbor Port ID           TTL
Et1        veos1                            Ethernet1                  120
veos2#ping 10.10.0.0
PING 10.10.0.0 (10.10.0.0) 72(100) bytes of data.
80 bytes from 10.10.0.0: icmp_seq=1 ttl=64 time=708 ms
80 bytes from 10.10.0.0: icmp_seq=2 ttl=64 time=148 ms
80 bytes from 10.10.0.0: icmp_seq=3 ttl=64 time=52.0 ms
80 bytes from 10.10.0.0: icmp_seq=4 ttl=64 time=796 ms
80 bytes from 10.10.0.0: icmp_seq=5 ttl=64 time=236 ms

--- 10.10.0.0 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 2484ms
rtt min/avg/max/mdev = 52.003/388.024/796.050/304.145 ms, pipe 2, ipg/ewma 621.038/548.983 ms
veos2#
</code></pre>
<p>Look at that! LLDP shows that both devices can see each other. Ping also works with no problems. Fully virtualized lab running in Docker, pretty neat :)</p>
<p>Here we used <code>vr-xcon</code> to create one connection between two devices but you can use single container to connect multiple devices. Or you can use however many containers you want, each providing just one connection, as long as names of the containers are different</p>
<p>What you do depends on your strategy and what you’re trying to achieve. Multiple containers providing connections can make more sense if you plan on simulating links going hard down. You can even start <code>vr-xcon</code> containers with mix of connections that reflect particular failure scenario that you want to test.</p>
<p>Some examples to show you different options.</p>
<p>Creating three links between three devices using one container:</p>
<pre><code class="language-shell">docker run -d --name vr-xcon --link veos1 --link veos2 --link veos3 vr-xcon --p2p veos1/2--veos2/2 veos1/10--veos3/10 veos2/5-veos3/5
</code></pre>
<p>And same links created using three instances of <code>vr-xcon</code> container:</p>
<pre><code class="language-shell">docker run -d --name vr-xcon1 --link veos1 --link veos2 vr-xcon --p2p veos1/2--veos2/2
docker run -d --name vr-xcon2 --link veos1 --link veos3 vr-xcon --p2p veos1/10--veos3/10
docker run -d --name vr-xcon3 --link veos2 --link veos3 vr-xcon --p2p veos2/5-veos3/5
</code></pre>
<p>Finally, to remove links, or links, you stop/remove the container used to create connections.</p>
<pre><code class="language-shell">przemek@quark:~$ docker rm -f vr-xcon
vr-xcon
</code></pre>
<p>For completeness, it's worth mentioning that <code>vr-xcon</code> also provides tap mode with <code>--tap-listen</code> argument. This allows other apps to be used with virtual routers. See Readme for <code>vr-xcon</code> in Vrnetlab repo for more details.</p>
<p>After you're done with your lab and want to get rid of container you should run <code>docker rm -f</code> command.</p>
<pre><code class="language-shell">docker rm -f veos1 veos2
</code></pre>
<p>And with that your containers will be gone.</p>
<p><a name="bring-up-pre-def"></a></p>
<h2 id="bringuppredefinedtopologywithdockercompose">Bring up pre-defined topology with Docker Compose</h2>
<p>You should now know how to bring containers by hand and how to connect them. It can get a bit tedious though if you often bring up the same topology for testing. Why not write some kind of lab recipe that we can launch with single command? Well, we can do that and we will!</p>
<p>There are many ways that you can achieve this and for our example we will use Docker Compose. We'll create <code>docker-compose.yml</code> file that will bring up two virtual images and connect them together. As a bonus we will expose our virtual devices to the external world. This could be useful for remote labbing.</p>
<p><strong>Note</strong>: If you don't have <code>docker-compose</code> installed you can get it by running <code>sudo apt install docker-compose</code> in Ubuntu. Other distros should have it readily available as well.</p>
<p>Compose uses <code>docker-compose.yml</code> file to define services to be run together.</p>
<p>I wrote one such file that will bring 2 vEOS devices with one link between them:</p>
<pre><code class="language-yaml">przemek@quark:~/netdev/dcompose/veos-lab$ cat docker-compose.yml 
version: &quot;3&quot;

services:
  veos1:
    image: veos:4.18.10M
    container_name: veos1
    privileged: true
    ports:
      - &quot;9001:22&quot;
    network_mode: default
  veos2:
    image: veos:4.18.10M
    container_name: veos2
    privileged: true
    ports:
      - &quot;9002:22&quot;
    network_mode: default
  vr-xcon:
    image: vr-xcon
    container_name: vr-xcon
    links:
      - veos1
      - veos2
    command: --p2p veos1/2--veos2/2
    depends_on:
      - veos1
      - veos2
    network_mode: default
</code></pre>
<p>If it's the first time you see <code>docker-compose.yml</code> don't worry, I'm breaking it down for you below.</p>
<ul>
<li>
<p>First line defines version of Compose format, <code>version: &quot;3&quot;</code> is pretty old but is widely supported.</p>
</li>
<li>
<p>In <code>services</code> section we define three containers that we want to be launched. First off we define <code>veos1</code> service.</p>
<pre><code class="language-yaml">  veos1:
    image: veos:4.18.10M
    container_name: veos1
    privileged: true
    ports:
      - &quot;9001:22&quot;
    network_mode: default
</code></pre>
<ul>
<li><code>veos1</code> - Name of our service where we define container.</li>
<li><code>image: veos:4.18.10M</code> - We want our container to use <code>veos:4.18.10M</code> image.</li>
<li><code>container_name: veos1</code> - Override default container name to make it easier to refer to later.</li>
<li><code>privileged: true</code> - We need privileged mode for KVM.</li>
<li><code>ports:</code> - We tell Docker Compose to use host port 9001 to connect to port 22 in container.<br>
<code>network_mode: default</code> - We ask Compose to use default Docker bridge, by default Docker Compose would create separate network and Vrnetlab doesn't like it.</li>
</ul>
</li>
<li>
<p>Definition for <code>veos2</code> is same except we map port 22 to port 9002 on host.</p>
</li>
<li>
<p>Finally, we have <code>vr-xcon</code> service. Items of note here:</p>
<ul>
<li><code>links:</code> - Equivalent to <code>--link</code> argument we used when running container by hand. Listed containers will be linked to <code>vr-xcon</code> container.</li>
<li><code>command: --p2p veos1/2--veos2/2</code> - This is how command is passed to container when using Docker Compose.<br>
<code>depends_on:</code> - We tell Compose to wait for the listed service, here <code>veos1</code> and <code>veos2</code>, before starting vr-xcon service.</li>
</ul>
</li>
</ul>
<p>With all that in place we're ready to launch our virtual lab using <code>docker-compose up -d</code> command, run in the directory that contains <code>docker-compose.yml</code>. Options <code>-d</code> makes Compose run in background.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/dcompose/veos-lab$ docker-compose rm
No stopped containers
przemek@quark:~/netdev/dcompose/veos-lab$ docker-compose up -d
Creating veos2 ... done
Creating veos1 ... done
Creating vr-xcon ... done
</code></pre>
<p>Now to check if containers are running:</p>
<pre><code class="language-shell">przemek@quark:~/netdev/dcompose/veos-lab$ docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS                    PORTS                                                                                NAMES
0ec9b3e8d4e9        vr-xcon                   &quot;/xcon.py --p2p veos…&quot;   30 minutes ago      Up 29 minutes                                                                                                  vr-xcon
3062da1b3417        veos:4.18.10M             &quot;/launch.py&quot;             30 minutes ago      Up 29 minutes (healthy)   80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp, 0.0.0.0:9001-&gt;22/tcp   veos1
4e81f0de7be7        veos:4.18.10M             &quot;/launch.py&quot;             30 minutes ago      Up 29 minutes (healthy)   80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp, 0.0.0.0:9002-&gt;22/tcp   veos2
</code></pre>
<p>Now we're talking! Entire mini lab launched with one command! And now that you know how this works you can extend the <code>docker-compose.yml</code> file to your liking. Want more devices? No problem, we'll define new veos service. Need extra links? Just define more services or add arguments to an existing one.</p>
<p>Let's quickly connect to the devices and configure hostnames. This will prove we can access them and it will help us identify devices later.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/dcompose/veos-lab$ vrcons veos1
Trying 172.17.0.5...
Connected to 172.17.0.5.
Escape character is '^]'.

localhost#conf t
localhost(config)#hostname veos1
veos1(config)#end
veos1#
telnet&gt; quit
Connection closed.
przemek@quark:~/netdev/dcompose/veos-lab$ vrcons veos2
Trying 172.17.0.4...
Connected to 172.17.0.4.
Escape character is '^]'.

localhost#conf t
localhost(config)#hostname veos2
veos2(config)#end
veos2#
telnet&gt; quit
Connection closed.
</code></pre>
<p>Yup, all accessible, no problems here.</p>
<p><a name="accessing-lab-fr"></a></p>
<h3 id="accessinglabfromoutside">Accessing lab from outside</h3>
<p>You might remember that I exposed port TCP22 on both of our devices. I then mapped it to local ports 9001 and 9002 for veos1 and veos2 respectively.</p>
<pre><code>przemek@quark:~/netdev/dcompose/veos-lab$ sudo lsof -nP -iTCP -sTCP:LISTEN | grep 900[12]
docker-pr 20748            root    4u  IPv6 179906      0t0  TCP *:9002 (LISTEN)
docker-pr 20772            root    4u  IPv6 185480      0t0  TCP *:9001 (LISTEN)
</code></pre>
<p>There we have it, two TCP ports, 9001 and 9002, listening on our machine.</p>
<pre><code>przemek@quark:~/netdev/dcompose/veos-lab$ ip -4 a show dev ens33
2: ens33: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.5.128/24 brd 192.168.5.255 scope global dynamic noprefixroute ens33
       valid_lft 1590sec preferred_lft 1590sec
</code></pre>
<p>IP address of interface on my VM is 192.168.5.128. So we have ports, we have IP, we should be able to connect right? Let's give it a go :)</p>
<p>I added entries for both of the devics to my terminal app in Windows.</p>
<p><img src="https://ttl255.com/content/images/2020/08/kitty-veos.PNG" alt="kitty-veos"></p>
<p>Let's try <code>veos1</code> first.</p>
<p><img src="https://ttl255.com/content/images/2020/09/veos1-ssh-fin.gif" alt="veos1-ssh-fin"></p>
<p>Hey, we managed to access our virtual device running in a Docker Container over the network! You can see that it is running virtual image and it can see <code>veos2</code> on one of its interfaces.</p>
<p>But, can we connect to <code>veos2</code> as well?</p>
<p><img src="https://ttl255.com/content/images/2020/08/veos2-ssh-fin.gif" alt="veos2-ssh-fin"></p>
<p>Oh yes, we can :) Again, it's running virtual image and can see <code>veos1</code>. These are most definetely the boxes we brought up with Docker Compose.</p>
<p>And think what you can already do with that. You can create a job in Jenkins building the entire lab for you when you make your morning coffee. It will all be ready and accessible over the network when you come back to your desk.</p>
<p>There's tremendous value and potential there for sure.</p>
<p>Finally, once you're done and want your lab to go away, you again need only one command, this time <code>docker-compose down</code>. This will stop the services defined in <code>docker-compose.yml</code> file and will remove them completely.</p>
<pre><code class="language-shell">przemek@quark:~/netdev/dcompose/veos-lab$ docker-compose down
Stopping veos-lab_vr-xcon_1 ... done
Stopping veos2              ... done
Stopping veos1              ... done
Removing veos-lab_vr-xcon_1 ... done
Removing veos2              ... done
Removing veos1              ... done
</code></pre>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>With that, we've come to an end of this post. I hope what you've seen here makes you as excited about Vrnetlab as I am. It might not have pretty GUI of other existing solutions out there, but it has its own unique strengths that I think make it better suited for Network Automation.</p>
<p>Once you built your collection of Docker images with virtual appliances it doesn't take much time to build your lab. Most importantly, you can describe your topology using code and store these files in GIT repository. This allows you to have multiple versions and track changes between deployments.</p>
<p>And that's not all. I have another post coming where I will use Vrnetlab with Ansible to build Network Automation CI pipelines. We'll have more code and more automation! I'm looking forward to that and I hope that you do too :)</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>
<p>Vrnetlab GitHub repository: <a href="https://github.com/plajjan/vrnetlab" target="_blank">https://github.com/plajjan/vrnetlab</a></p>
</li>
<li>
<p>Michael Kashin shows you how to run Vrnetlab at scale with Kubernetes: <a href="https://networkop.co.uk/post/2019-01-k8s-vrnetlab/">https://networkop.co.uk/post/2019-01-k8s-vrnetlab/</a></p>
</li>
<li>
<p>Other great post on Vrnetlab, has pretty diagrams: <a href="https://www.brianlinkletter.com/vrnetlab-emulate-networks-using-kvm-and-docker/">https://www.brianlinkletter.com/vrnetlab-emulate-networks-using-kvm-and-docker/</a></p>
</li>
<li>
<p>GitHub repo with resources for this post. Available at: <a href="https://github.com/progala/ttl255.com/tree/master/vrnetlab/vrnetlab-router-in-docker" target="_blank">https://github.com/progala/ttl255.com/tree/master/vrnetlab/vrnetlab-router-in-docker</a></p>
</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Jinja2 Tutorial - Part 4 - Template filters]]></title><description><![CDATA[In this post we focus on Jinja2 filters. We learn what filters are and how to use them. Then you will see how to write custom filters. We finish with usage examples for selected filters.]]></description><link>https://ttl255.com/jinja2-tutorial-part-4-template-filters/</link><guid isPermaLink="false">5f15e4791e69ff52c5b065be</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[automation]]></category><category><![CDATA[python]]></category><category><![CDATA[Ansible]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Tue, 21 Jul 2020 08:14:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>This is part 4 of Jinja2 tutorial where we continue looking at the language features, specifically we'll be discussing template filters. We'll see what filters are and how we can use them in our templates. I'll also show you how you can write your own custom filters.</p>
<h2 id="jinja2tutorialseries">Jinja2 Tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/jinja2-tutorial-part-1-introduction-and-variable-substitution/">Jinja2 Tutorial - Part 1 - Introduction and variable substitution</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-2-loops-and-conditionals/">Jinja2 Tutorial - Part 2 - Loops and conditionals</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-3-whitespace-control/">Jinja2 Tutorial - Part 3 - Whitespace control</a></li>
<li>Jinja2 Tutorial - Part 4 - Template filters</li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-5-macros/">Jinja2 Tutorial - Part 5 - Macros</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-6-include-and-import/">Jinja2 Tutorial - Part 6 - Include and Import</a></li>
<li><a href="https://ttl255.com/j2live-online-jinja2-parser/">J2Live - Online Jinja2 Parser</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#filt-over">Overview of Jinja2 filters</a></li>
<li><a href="#mult-args">Multiple arguments</a></li>
<li><a href="#chain-filt">Chaining filters</a></li>
<li><a href="#add-cust-filt">Additional filters and custom filters</a></li>
<li><a href="#why-filt">Why use filters?</a></li>
<li><a href="#when-not">When not to use filters?</a></li>
<li><a href="#write-custom">Writing your own filters</a></li>
<li><a href="#fix-clever">Fixing <em>&quot;too clever&quot;</em> solution with Ansible custom filter</a></li>
<li><a href="#cust-ans">Custom filters in Ansible</a></li>
<li><a href="#usage-ex">Jinja2 Filters - Usage examples</a>
<ul>
<li><a href="#batch">batch</a></li>
<li><a href="#center">center</a></li>
<li><a href="#default">default</a></li>
<li><a href="#dictsort">dictsort</a></li>
<li><a href="#float">float</a></li>
<li><a href="#groupby">groupby</a></li>
<li><a href="#int">int</a></li>
<li><a href="#join">join</a></li>
<li><a href="#map">map</a></li>
<li><a href="#reject">reject</a></li>
<li><a href="#rejectattr">rejectattr</a></li>
<li><a href="#select">select</a></li>
<li><a href="#tojson">tojson</a></li>
<li><a href="#unique">unique</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p4-template-filters" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="filt-over"></a></p>
<h2 id="overviewofjinja2filters">Overview of Jinja2 filters</h2>
<p>Let's jump straight in. Jinja2 filter is something we use to transform data held in variables. We apply filters by placing pipe symbol <code>|</code> after the variable followed by name of the filter.</p>
<p>Filters can change the look and format of the source data, or even generate new data derived from the input values. What's important is that the original data is replaced by the result of transformations and that's what ends up in rendered templates.</p>
<p>Here's an example showing a simple filter in action:</p>
<p>Template:</p>
<pre><code class="language-text">First name: {{ first_name | capitalize }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">first_name: przemek
</code></pre>
<p>Result:</p>
<pre><code class="language-text">First name: Przemek
</code></pre>
<p>We passed <code>first_name</code> variable to <code>capitalize</code> filter. As the name of the filter suggests, string held by variable will end up capitalized. And this is exactly what we can see happened. Pretty cool, right?</p>
<p>It might help to think of filters as functions that take Jinja2 variable as argument, the only difference to standard Python functions is syntax that we use.</p>
<p>Python equivalent of <code>capitalize</code> would look like this:</p>
<pre><code class="language-python">def capitalize(word):
    return word.capitalize()

first_name = &quot;przemek&quot;

print(&quot;First name: {}&quot;.format(capitalize(first_name)))
</code></pre>
<p>Great, you say. But how did I know that <code>capitalize</code> is a filter? Where did it come from?</p>
<p>There's no magic here. Someone had to code all of those filters and make them available to us. Jinja2 comes with a number of useful filters, <code>capitalize</code> is one of them.</p>
<p>All of the built-in filters are documented in official Jinja2 docs. I'm including link in <a href="#references">references</a> and later in this post I'll show examples of some of the more useful, in my opinion, filters.</p>
<p><a name="mult-args"></a></p>
<h2 id="multiplearguments">Multiple arguments</h2>
<p>We're not limited to simple filters like <code>capitalize</code>. Some filters can take extra arguments in parentheses. These can be either keyword or positional arguments.</p>
<p>Below is an example of a filter taking extra argument.</p>
<p>Template:</p>
<pre><code class="language-text">ip name-server {{ name_servers | join(&quot; &quot;) }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">name_servers:
  - 1.1.1.1
  - 8.8.8.8
  - 9.9.9.9
  - 8.8.4.4
</code></pre>
<p>Result:</p>
<pre><code class="language-text">ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4
</code></pre>
<p>Filter <code>join</code> took list stored in <code>name_servers</code> and created a string by gluing together elements of the list with space as a separator. Separator is the argument we supplied in parenthesis and we could use different one depending on our needs.</p>
<p>You should refer to documentation to find out what arguments, if any, are available for given filter. Most filters use reasonable defaults and don't require all of the arguments to be explicitly specified.</p>
<p><a name="chain-filt"></a></p>
<h2 id="chainingfilters">Chaining filters</h2>
<p>We've seen basic filter usage but we can do more. We can chain filters together. This means that multiple filters can be used at once, each separated by pipe <code>|</code>.</p>
<p>Jinja applies chained filters from left to right. Value that comes out of leftmost filter is fed into the next one, and the process is repeated until there are no more filters. Only the final result will end up in rendered template.</p>
<p>Let's have a look at how it works.</p>
<p>Data:</p>
<pre><code class="language-text">scraped_acl:
  - &quot;   10 permit ip 10.0.0.0/24 10.1.0.0/24&quot;
  - &quot;   20 deny ip any any&quot;
</code></pre>
<p>Template</p>
<pre><code class="language-text">{{ scraped_acl | first | trim }}
</code></pre>
<p>Result</p>
<pre><code class="language-text">10 permit ip 10.0.0.0/24 10.1.0.0/24
</code></pre>
<p>We passed list containing two items to <code>first</code> filter. This returned first element from the list and handed it over to <code>trim</code> filter which removed leading spaces.</p>
<p>The end result is line <code>10 permit ip 10.0.0.0/24 10.1.0.0/24</code>.</p>
<p>Filter chaining is a powerful feature that allows us to perform multiple transformations in one go. The alternative would be to store intermediate results which would decrease readability and wouldn't be as elegant.</p>
<p><a name="add-cust-filt"></a></p>
<h2 id="additionalfiltersandcustomfilters">Additional filters and custom filters</h2>
<p>Great as they are, built-in filters are very generic and many use cases call for more specific ones. This is why automation frameworks like Ansible or Salt provide many extra filters that cover wide range of scenarios.</p>
<p>In these frameworks you will find filters that can transform IP objects, display data in YAML/Json, or even apply regex, just to name a few. In <a href="#references">references</a> you can find links to  docs for filters available in each framework.</p>
<p>Finally, you can create new filters yourself! Jinja2 provides hooks for adding custom filters. These are just Python functions, so if you wrote Python function before you will be able to write your own filter as well!</p>
<p>Aforementioned automation frameworks also support custom filters and the process of writing them is similar to vanilla Jinja2. You again need to write a Python function and then documentation for given tool will show you steps needed to register your module as a filter.</p>
<p><a name="why-filt"></a></p>
<h2 id="whyusefilters">Why use filters?</h2>
<p>No tool is a good fit for every problem. And some tools are solutions in search of a problem. So, why use Jinja2 filters?</p>
<p>Jinja, like most of the templating languages, was created with web content in mind. While data is stored in the standardized format in the database, we often need to transform it when displaying documents to the users. This is where language like Jinja with its filters enables on the go modification to the way data is presented, without having to touch back-end. That's the selling point of filters.</p>
<p>Below is my personal view on why I think Jinja2 filters are a good addition to the language:</p>
<p><strong>1. They allow non-programmers to perform simple data transformations.</strong></p>
<p>This applies to vanilla filters as well as extra filters provided by automation frameworks. For example, network engineers know their IP addresses and they might want to operate on them in templates without having any programming knowledge. Filters to the rescue!</p>
<p><strong>2. You get predictable results.</strong></p>
<p>If you use generally available filters anyone with some Jinja2 experience will know what they do. This allows people to get up to speed when reviewing templates written by others.</p>
<p><strong>3. Filters are well maintained and tested.</strong></p>
<p>Built-in filters as well as filters provided by automation frameworks are widely used by a lot people. This gives you high confidence that they give correct results and don't have many bugs.</p>
<p><strong>4. The best code is no code at all.</strong></p>
<p>The moment you add data transformation operations to your program, or create a new filter, you become responsible for the code, forever. Any bugs, feature requests, and tests will come your way for the lifetime of the solution. Write as much stuff as you want when learning but use already available solutions in production, whenever possible.</p>
<p><a name="when-not"></a></p>
<h2 id="whennottousefilters">When not to use filters?</h2>
<p>Filters can be really powerful and save us a lot of time. But with great power comes great responsibility. Overuse filters and you can end up with templates that are difficult to understand and maintain.</p>
<p>You know those clever one liners that no one, including yourself, can understand few months down the line? It's very easy to get into those situations with chaining a lot of filters, especially ones accepting multiple arguments.</p>
<p>I use the below heuristics to help me decide if what I did is too complicated:</p>
<ul>
<li>Is what I wrote at the limit of my understanding?</li>
<li>Do I feel like what I just wrote is really clever?</li>
<li>Did I use many chained filters in a way that didn't seem obvious at first?</li>
</ul>
<p>If you answer yes to at least one of the above, you might be dealing with the case of <em>too clever for your own good</em>. It is possible that there's no good simpler solution for your use case, but chances are you need to do refactoring. If you're unsure if that's the case it's best to ask your colleagues or check with community.</p>
<p>To show you how bad things can get, here's an example of Jinja2 lines I wrote a few years ago. These use filters provided by Ansible and it got so complicated that I had to define intermediate variables.</p>
<p>Have a look at it and try to figure what does it do, and more importantly, how does it do it.</p>
<p>Template, cut down for brevity:</p>
<pre><code class="language-text">{% for p in ibgp %}
{%  set jq = &quot;[?name=='&quot; + p.port + &quot;'].{ myip: ip, peer: peer }&quot; %}
{%  set el = ports | json_query(jq) %}
{%  set peer_ip = hostvars[el.0.peer] | json_query('ports[*].ip') | ipaddr(el.0.myip) %}
...
{%  endfor %}
</code></pre>
<p>Example data used with the template:</p>
<pre><code class="language-text">ibgp:
  - { port: Ethernet1 }
  - { port: Ethernet2 }
..
ports:
  - { name: Ethernet1, ip: &quot;10.0.12.1/24&quot;, speed: 1000full, desc: &quot;vEOS-02
</code></pre>
<p>There's so much to unpack here. In the first line I assign query string to a variable as a workaround for character escaping issues. In line two I apply <code>json_query</code> filter with argument coming from variable in line one, the result is stored in another helper variable. Finally, in the line three I apply two chained filters <code>json_query</code> and <code>ipaddr</code>.</p>
<p>The end result of these three lines should be IP address of BGP peer found on given interface.</p>
<p>I'm sure you will agree with me that this is terrible. Does this solution get any ticks next to heuristics I mentioned earlier? Yes! Three of them! This is a prime candidate for refactoring.</p>
<p>There are generally two things we can do in cases such as this:</p>
<ul>
<li>Pre-process data in the upper layer that calls rendering, e.g. Python, Ansible, etc.</li>
<li>Write a custom filter.</li>
<li>Revise the data model to see if it can be simplified.</li>
</ul>
<p>In this case I went with option 2, I wrote my own filter, which luckily is the next topic on our list.</p>
<p><a name="write-custom"></a></p>
<h2 id="writingyourownfilters">Writing your own filters</h2>
<p>As I already mentioned, to write a custom filter you need to get your hands dirty and write some Python code. Have no fear however! If you ever wrote a function taking an argument you've got all it takes. That's right, we don't need to do anything too fancy, any regular Python function can become a filter. It just needs to take at least one argument and it must return something.</p>
<p>Here's an example of function that we will register with Jinja2 engine as a filter:</p>
<pre><code># hash_filter.py
import hashlib


def j2_hash_filter(value, hash_type=&quot;sha1&quot;):
    &quot;&quot;&quot;
    Example filter providing custom Jinja2 filter - hash

    Hash type defaults to 'sha1' if one is not specified

    :param value: value to be hashed
    :param hash_type: valid hash type
    :return: computed hash as a hexadecimal string
    &quot;&quot;&quot;
    hash_func = getattr(hashlib, hash_type, None)

    if hash_func:
        computed_hash = hash_func(value.encode(&quot;utf-8&quot;)).hexdigest()
    else:
        raise AttributeError(
            &quot;No hashing function named {hname}&quot;.format(hname=hash_type)
        )

    return computed_hash
</code></pre>
<p>In Python this is how we tell Jinja2 about our filter:</p>
<pre><code># hash_filter_test.py
import jinja2
from hash_filter import j2_hash_filter

env = jinja2.Environment()
env.filters[&quot;hash&quot;] = j2_hash_filter

tmpl_string = &quot;&quot;&quot;MD5 hash of '$3cr3tP44$$': {{ '$3cr3tP44$$' | hash('md5') }}&quot;&quot;&quot;

tmpl = env.from_string(tmpl_string)

print(tmpl.render())
</code></pre>
<p>Result of rendering:</p>
<pre><code class="language-text">MD5 hash of '$3cr3tP44$$': ec362248c05ae421533dd86d86b6e5ff
</code></pre>
<p>Look at that! Our very own filter! It looks and feels just like built-in Jinja filters, right?</p>
<p>And what does it do? It exposes hashing functions from Python's <code>hashlib</code> library to allow for direct use of hashes in Jinja2 templates. Pretty neat if you ask me.</p>
<p>To put it in words, below are the steps needed to create custom filter:</p>
<ol>
<li>Create a function taking at least one argument, that returns a value. First argument is always the Jinja variable preceding <code>|</code> symbol. Subsequent arguments are provided in parentheses <code>(...)</code>.</li>
<li>Register the function with Jinja2 Environment. In Python insert your function into <code>filters</code> dictionary, which is an attribute of <code>Environment</code> object. Key name is what you want your filter to be called, here <code>hash</code>, and value is your function.</li>
<li>You can now use your filter same as any other Jinja filter.</li>
</ol>
<p><a name="fix-clever"></a></p>
<h2 id="fixingtoocleversolutionwithansiblecustomfilter">Fixing <em>&quot;too clever&quot;</em> solution with Ansible custom filter</h2>
<p>We know how to write custom filters, so now I can show you how I replaced part of my template where I went too far with clever tricks.</p>
<p>Here is my custom filter in its fully glory:</p>
<pre><code># get_peer_info.py
import ipaddress


def get_peer_info(our_port_info, hostvars):
    peer_info = {&quot;name&quot;: our_port_info[&quot;peer&quot;]}
    our_net = ipaddress.IPv4Interface(our_port_info[&quot;ip&quot;]).network
    peer_vars = hostvars[peer_info[&quot;name&quot;]]

    for _, peer_port_info in peer_vars[&quot;ports&quot;].items():
        if not peer_port_info[&quot;ip&quot;]:
            continue
        peer_net_obj = ipaddress.IPv4Interface(peer_port_info[&quot;ip&quot;])
        if our_net == peer_net_obj.network:
            peer_info[&quot;ip&quot;] = peer_net_obj.ip
            break
    return peer_info


class FilterModule(object):

    def filters(self):
        return {
            'get_peer_info': get_peer_info
        } 
</code></pre>
<p>First part is something you've seen before, it's a Python function taking two arguments and returning one value. Sure, it's longer than <em>&quot;clever&quot;</em> three-liner, but it's so much more readable.</p>
<p>There's more structure here, variables have meaningful names and I can tell what it's doing pretty much right away. More importantly, I know how it's doing it, process is broken down into many individual steps that are easy to follow.</p>
<p><a name="cust-ans"></a></p>
<h2 id="customfiltersinansible">Custom filters in Ansible</h2>
<p>The second part of my solution is a bit different than vanilla Python example:</p>
<pre><code>class FilterModule(object):

    def filters(self):
        return {
            'get_peer_info': get_peer_info
        } 
</code></pre>
<p>This is how you tell Ansible that you want <code>get_peer_info</code> to be registered as a  Jinja2 filter.</p>
<p>You create class named <code>FilterModule</code> with one method called <code>filters</code>. This method must return dictionary with your filters. Keys in dictionary are names of the filters and values are functions. I say filter(s) and not filter, because you can register multiple filters in one file. Optionally you can have one filter per file if you prefer.</p>
<p>Once all is done you need to drop your Python module in <code>filter_plugins</code> directory, which should be located in the root of your repository. With that in place you can use your filters in Ansible Playbooks as well as Jinja2 templates.</p>
<p>Below you can see structure of the directory where my playbook <code>deploy_base.yml</code> is located in relation to the <code>get_peer_info.py</code> module.</p>
<pre><code class="language-text">.
├── ansible.cfg
├── deploy_base.yml
├── filter_plugins
│   └── get_peer_info.py
├── group_vars
    ...
├── hosts
├── host_vars
    ...
└── roles
    └── base
</code></pre>
<p><a name="usage-ex"></a></p>
<h2 id="jinja2filtersusageexamples">Jinja2 Filters - Usage examples</h2>
<p>All of the Jinja2 filters are well documented in officials docs but I felt that some of them could use some more examples. Below you will find my subjective selection with some comments and explanations.</p>
<p><a name="batch"></a></p>
<h3 id="batch">batch</h3>
<p><code>batch(value, linecount, fill_with=None)</code> - Allows you to group list elements into multiple buckets, each containing up to <strong>n</strong> elements, where <strong>n</strong> is number we specify. Optionally we can also ask <code>batch</code> to pad bucket with default entries to make all of the buckets exactly <strong>n</strong> in length. Result is list of lists.</p>
<p>I find it handy for splitting items into groups of fixed size.</p>
<p>Template:</p>
<pre><code class="language-text">{% for i in  sflow_boxes|batch(2) %}
Sflow group{{ loop.index }}: {{ i | join(', ') }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">sflow_boxes:
 - 10.180.0.1
 - 10.180.0.2
 - 10.180.0.3
 - 10.180.0.4
 - 10.180.0.5
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Sflow group1: 10.180.0.1, 10.180.0.2
Sflow group2: 10.180.0.3, 10.180.0.4
Sflow group3: 10.180.0.5
</code></pre>
<p><a name="center"></a></p>
<h3 id="center">center</h3>
<p><code>center(value, width=80)</code>- Centers value in a field of given width by adding space padding. Handy when adding formatting to reporting.</p>
<p>Template:</p>
<pre><code class="language-text">{{ '-- Discovered hosts --' | center }}
{{ hosts | join('\n') }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">hosts:
 - 10.160.0.7
 - 10.160.0.9
 - 10.160.0.3
</code></pre>
<p>Result:</p>
<pre><code class="language-text">                             -- Discovered hosts --                             
10.160.0.7
10.160.0.9
10.160.0.15
</code></pre>
<p><a name="default"></a></p>
<h3 id="default">default</h3>
<p><code>default(value, default_value='', boolean=False)</code> - Returns default value if passed variable is not specified. Useful for guarding against undefined variables. Can also be used for <em>optional</em> attribute that we want to set to sane value as a default.</p>
<p>In below example we place interfaces in their configured vlans, or if no vlan is specified we assign them to vlan 10 by default.</p>
<p>Template:</p>
<pre><code class="language-text">{% for intf in interfaces %}
interface {{ intf.name }}
  switchport mode access
  switchport access vlan {{ intf.vlan | default('10') }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
  - name: Ethernet4
</code></pre>
<p>Result:</p>
<pre><code class="language-text">interface Ethernet1
  switchport mode access
  switchport access vlan 50
interface Ethernet2
  switchport mode access
  switchport access vlan 50
interface Ethernet3
  switchport mode access
  switchport access vlan 10
interface Ethernet4
  switchport mode access
  switchport access vlan 10
</code></pre>
<p><a name="dictsort"></a></p>
<h3 id="dictsort">dictsort</h3>
<p><code>dictsort(value, case_sensitive=False, by='key', reverse=False)</code> - Allows us to sort dictionaries as they are not sorted by default in Python. Sorting is done by <em>key</em> by default but you can request sorting by <em>value</em> using attribute <code>by='value'</code>.</p>
<p>In below example we sort prefix-lists by their name (dict key):</p>
<p>Template:</p>
<pre><code class="language-text">{% for pl_name, pl_lines in prefix_lists | dictsort %}
ip prefix list {{ pl_name }}
  {{ pl_lines | join('\n') }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">prefix_lists:
  pl-ntt-out:
    - permit 10.0.0.0/23
  pl-zayo-out:
    - permit 10.0.1.0/24
  pl-cogent-out:
    - permit 10.0.0.0/24
</code></pre>
<p>Result:</p>
<pre><code class="language-text">ip prefix list pl-cogent-out
  permit 10.0.0.0/24
ip prefix list pl-ntt-out
  permit 10.0.0.0/23
ip prefix list pl-zayo-out
  permit 10.0.1.0/24
</code></pre>
<p>And here we order some peer list by priority (dict value), with higher values being more preferred, hence use of <code>reverse=true</code>:</p>
<p>Template:</p>
<pre><code class="language-text">BGP peers by priority

{% for peer, priority in peer_priority | dictsort(by='value', reverse=true) %}
Peer: {{ peer }}; priority: {{ priority }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">peer_priority:
  ntt: 200
  zayo: 300
  cogent: 100
</code></pre>
<p>Result:</p>
<pre><code class="language-text">BGP peers by priority

Peer: zayo; priority: 300
Peer: ntt; priority: 200
Peer: cogent; priority: 100
</code></pre>
<p><a name="float"></a></p>
<h3 id="float">float</h3>
<p><code>float(value, default=0.0)</code> - Converts the value to float number. Numeric values in API responses sometimes come as strings. With <code>float</code> we can make sure string is converted before making comparison.</p>
<p>Here's an example of software version checking that uses <code>float</code>.</p>
<p>Template:</p>
<pre><code class="language-text">{% if eos_ver | float &gt;= 4.22 %}
Detected EOS ver {{ eos_ver }}, using new command syntax.
{% else %}
Detected EOS ver {{ eos_ver }}, using old command syntax.
{% endif %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">eos_ver: &quot;4.10&quot;
</code></pre>
<p>Result</p>
<pre><code class="language-text">Detected EOS ver 4.10, using old command syntax.
</code></pre>
<p><a name="groupby"></a></p>
<h3 id="groupby">groupby</h3>
<p><code>groupby(value, attribute)</code> - Used to group objects based on one of the attributes. You can choose to group by nested attribute using dot notation. This filter can be used for reporting based on feature value or selecting items for an operation that is only applicable to a subset of objects.</p>
<p>In the below example we group interfaces based on the vlan they're assigned to:</p>
<p>Template:</p>
<pre><code class="language-text">{% for vid, members in  interfaces | groupby(attribute='vlan') %}
Interfaces in vlan {{ vid }}: {{ members | map(attribute='name') | join(', ') }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Interfaces in vlan 50: Ethernet1, Ethernet2, Ethernet3
Interfaces in vlan 60: Ethernet4
</code></pre>
<p><a name="int"></a></p>
<h3 id="int">int</h3>
<p><code>int(value, default=0, base=10)</code> - Same as float but here we convert value to integer. Can be also used for converting other bases into decimal base:</p>
<p>Example below shows hexadecimal to decimal conversion.</p>
<p>Template:</p>
<pre><code class="language-text">LLDP Ethertype
hex: {{ lldp_ethertype }} 
dec: {{ lldp_ethertype | int(base=16) }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">lldp_ethertype: 88CC
</code></pre>
<p>Result:</p>
<pre><code class="language-text">LLDP Ethertype
hex: 88CC 
dec: 35020
</code></pre>
<p><a name="join"></a></p>
<h3 id="join">join</h3>
<p><code>join(value, d='', attribute=None)</code> - Very, very useful filter. Takes elements of the sequence and returns concatenated elements as a string.</p>
<p>For cases when you just want to display items, without applying any operations, it can replace <code>for</code> loop. I find <code>join</code> version more readable in these cases.</p>
<p>Template:</p>
<pre><code class="language-text">ip name-server {{ name_servers | join(&quot; &quot;) }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">name_servers:
  - 1.1.1.1
  - 8.8.8.8
  - 9.9.9.9
  - 8.8.4.4
</code></pre>
<p>Result:</p>
<pre><code class="language-text">ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4
</code></pre>
<p><a name="map"></a></p>
<h3 id="map">map</h3>
<p><code>map(*args, **kwargs)</code> - Can be used to look up an attribute or apply filter on all objects in the sequence.</p>
<p>For instance if you want to normalize letter casing across device names you could apply filter in one go.</p>
<p>Template:</p>
<pre><code class="language-text">Name-normalized device list:
{{ devices | map('lower') | join('\n') }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">devices:
 - Core-rtr-warsaw-01
 - DIST-Rtr-Prague-01
 - iNET-rtR-berlin-01
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Name-normalized device list:
core-rtr-warsaw-01
dist-rtr-prague-01
Inet-rtr-berlin-01
</code></pre>
<p>Personally I find it most useful for retrieving attributes and their values across a large number of objects. Here we're only interested in values of <code>name</code> attribute:</p>
<p>Template:</p>
<pre><code class="language-text">Interfaces found:
{{ interfaces | map(attribute='name') | join('\n') }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    mode: switched
  - name: Ethernet2
    mode: switched
  - name: Ethernet3
    mode: routed
  - name: Ethernet4
    mode: switched
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Interfaces found:
Ethernet1
Ethernet2
Ethernet3
Ethernet4
</code></pre>
<p><a name="reject"></a></p>
<h3 id="reject">reject</h3>
<p><code>reject(*args, **kwargs)</code> - Filters sequence of items by applying a Jinja2 test and rejecting objects succeeding the test. That is item will be removed from the final list if result of the test is <code>true</code>.</p>
<p>Here we want to display only public BGP AS numbers.</p>
<p>Template:</p>
<pre><code class="language-text">Public BGP AS numbers:
{% for as_no in as_numbers| reject('gt', 64495) %}
{{ as_no }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">as_numbers:
 - 1794
 - 28910
 - 65203
 - 64981
 - 65099
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Public BGP AS numbers:
1794
28910
</code></pre>
<p><a name="rejectattr"></a></p>
<h3 id="rejectattr">rejectattr</h3>
<p><code>rejectattr(*args, **kwargs)</code> - Same as <code>reject</code> filter but test is applied to the selected attribute of the object.</p>
<p>If your chosen test takes arguments, provide them after test name, separated by commas.</p>
<p>In this example we want to remove 'switched' interfaces from the list by applying test to the 'mode' attribute.</p>
<p>Template:</p>
<pre><code class="language-text">Routed interfaces:

{% for intf in interfaces | rejectattr('mode', 'eq', 'switched') %}
{{ intf.name }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    mode: switched
  - name: Ethernet2
    mode: switched
  - name: Ethernet3
    mode: routed
  - name: Ethernet4
    mode: switched
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Routed interfaces:

Ethernet3
</code></pre>
<p><a name="select"></a></p>
<h3 id="select">select</h3>
<p><code>select(*args, **kwargs)</code> - Filters the sequence by retaining only the elements passing the Jinja2 test. This filter is the opposite of <code>reject</code>. You can use either of those depending on what feels more natural in given scenario.</p>
<p>Similarly to <code>reject</code> there's also <code>selectattr</code> filter that works the same as <code>select</code> but is applied to the attribute of each object.</p>
<p>Below we want to report on private BGP AS numbers found on our device.</p>
<p>Template:</p>
<pre><code class="language-text">Private BGP AS numbers:
{% for as_no in as_numbers| select('gt', 64495) %}
{{ as_no }}
{% endfor %}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">as_numbers:
 - 1794
 - 28910
 - 65203
 - 64981
 - 65099
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Private BGP AS numbers:
65203
64981
65099
</code></pre>
<p><a name="tojson"></a></p>
<h3 id="tojson">tojson</h3>
<p><code>tojson(value, indent=None)</code> - Dumps data structure in JSON format. Useful when rendered template is consumed by application expecting JSON. Can be also used as an alternative to <code>pprint</code> for prettifying variable debug output.</p>
<p>Template:</p>
<pre><code class="language-text">{{ interfaces | tojson(indent=2) }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60
</code></pre>
<p>Result:</p>
<pre><code class="language-text">[
  {
    &quot;name&quot;: &quot;Ethernet1&quot;,
    &quot;vlan&quot;: 50
  },
  {
    &quot;name&quot;: &quot;Ethernet2&quot;,
    &quot;vlan&quot;: 50
  },
  {
    &quot;name&quot;: &quot;Ethernet3&quot;,
    &quot;vlan&quot;: 50
  },
  {
    &quot;name&quot;: &quot;Ethernet4&quot;,
    &quot;vlan&quot;: 60
  }
]
</code></pre>
<p><a name="unique"></a></p>
<h3 id="unique">unique</h3>
<p><code>unique(value, case_sensitive=False, attribute=None)</code> - Returns list of unique values in given collection. Pairs well with <code>map</code> filter for finding set of values used for given attribute.</p>
<p>Here we're finding which access vlans we use across our interfaces.</p>
<p>Template:</p>
<pre><code class="language-text">Access vlans in use: {{ interfaces | map(attribute='vlan') | unique | join(', ') }}
</code></pre>
<p>Data:</p>
<pre><code class="language-text">interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60
</code></pre>
<p>Result:</p>
<pre><code class="language-text">Access vlans in use: 50, 60
</code></pre>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>And with this fairly long list of examples we came to the end of this part of the tutorial. Jinja2 filters can be a very powerful tool in right hands and I hope that my explanations helped you in seeing their potential.</p>
<p>You do need to remember to use them judiciously, if it starts looking unwieldy and doesn't feel right, look at alternatives. See if you can move complexity outside of the template, revise your data model, or if that's not possible, write your own filter.</p>
<p>That's all from me. As always, I look forward to seeing you again, more Jinja2 posts are coming soon!</p>
<p><a name="references"></a></p>
<h2 id="references">References</h2>
<ul>
<li>Jinja2 built-in filters, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters</a></li>
<li>Jinja2 custom filters, official docs: <a href="https://jinja.palletsprojects.com/en/2.11.x/api/#custom-filters" target="_blank">https://jinja.palletsprojects.com/en/2.11.x/api/#custom-filters</a></li>
<li>All filters available in Ansible, official docs: <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html" target="_blank">https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html</a></li>
<li>All filters available in Salt, official docs: <a href="https://docs.saltstack.com/en/latest/topics/jinja/index.html#filters" target="_blank">https://docs.saltstack.com/en/latest/topics/jinja/index.html#filters</a></li>
<li>GitHub repo with resources for this post. Available at: <a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p4-template-filters" target="_blank">https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p4-template-filters</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[YAML anchors and aliases and how to disable them]]></title><description><![CDATA[In this post we're looking at YAML anchors and aliases. We also look at different ways of stopping PyYAML from using references when serializing data structures.]]></description><link>https://ttl255.com/yaml-anchors-and-aliases-and-how-to-disable-them/</link><guid isPermaLink="false">5f01e2491e69ff52c5b065b0</guid><category><![CDATA[YAML]]></category><category><![CDATA[python]]></category><category><![CDATA[automation]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 05 Jul 2020 19:23:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h2 id="introduction">Introduction</h2>
<p>In this short post I explain what are YAML aliases and anchors. I then show you how to stop PyYAML from using these when serializing data structures.</p>
<p>While references are absolutely fine to use in YAML files meant for programmatic consumption I find that it sometimes confuses humans, especially if they’ve never seen these before. For this reason I tend to disable anchors and aliases when saving data to YAML files meant for human consumption.</p>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#yaml-refs">YAML aliases and anchors</a></li>
<li><a href="#refs-when">When does YAML use anchors and aliases</a></li>
<li><a href="#yaml-dump-no-refs">YAML dump - don’t use anchors and aliases</a>
<ul>
<li><a href="#deepcopy">Using copy.deepcopy() function</a></li>
<li><a href="#override">Overriding <code>ignore_aliases()</code> method</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/yaml/anchors-and-aliases" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="yaml-refs"></a></p>
<h2 id="yamlaliasesandanchors">YAML aliases and anchors</h2>
<p>YAML specification has provision for preserving information about nodes pointing to the same data. This basically means that if you have some data that is referenced in multiple places in your data structure then YAML dumper will:</p>
<ul>
<li>add an anchor to the first occurrence</li>
<li>replace any subsequent occurrences of that data with aliases</li>
</ul>
<p>Now, how do these anchors and aliases look like?</p>
<p><code>&amp;id001</code> - example of an anchor, placed with the first occurrence of data<br>
<code>*id001</code> - example of an alias, replaces subsequent occurrence of data</p>
<p>This might be easier to see on a concrete example. Below we have information about some interfaces. You can see that <em>Ethernet1</em> has anchor <code>&amp;id001</code> next to its <em>properties</em> key and <em>Ethernet2</em> has just alias <code>*id001</code> next to its <em>properties</em> key.</p>
<pre><code class="language-text">Ethernet1:
  description: Uplink to core-1
  mtu: 9000
  properties: &amp;id001
  - pim
  - ptp
  - lldp
  speed: 1000
Ethernet2:
  description: Uplink to core-2
  mtu: 9000
  properties: *id001
  speed: 1000
</code></pre>
<p>When we load this data in Python and print it we get the below:</p>
<pre><code class="language-python">{'Ethernet1': {'description': 'Uplink to core-1',
               'mtu': 9000,
               'properties': ['pim', 'ptp', 'lldp'],
               'speed': 1000},
 'Ethernet2': {'description': 'Uplink to core-2',
               'mtu': 9000,
               'properties': ['pim', 'ptp', 'lldp'],
               'speed': 1000}}
</code></pre>
<p>Anchor <code>&amp;id001</code> is gone and alias <code>*id001</code> was expanded into <code>['pim', 'ptp', 'lldp']</code>.</p>
<p><a name="refs-when"></a></p>
<h2 id="whendoesyamluseanchorsandaliases">When does YAML use anchors and aliases</h2>
<p>Dumper used by PyYAML can recognize Python variables and data structures pointing to the same object. This can often happen with deeply nested dictionaries with keys that refer to the same piece of data. A lot of APIs in the world of Network Automation use such dictionaries.</p>
<p>It’s worth pointing out that section 3.1.1 of YAML spec requires anchors and aliases to be used when serializing multiple references to the same node (data object). I will show how to override this behaviour but it’s good to know where it came from.</p>
<p>I wrote two, nearly identical, programs creating data structure from the beginning of this post. These will help us in understanding when PyYAML adds anchors and aliases.</p>
<p>Program #1:</p>
<pre><code class="language-python"># yaml_diff_ids.py
import yaml


interfaces = dict(
    Ethernet1=dict(description=&quot;Uplink to core-1&quot;, speed=1000, mtu=9000),
    Ethernet2=dict(description=&quot;Uplink to core-2&quot;, speed=1000, mtu=9000),
)

interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]

# Show IDs referenced by &quot;properties&quot; key
print(&quot;Ethernet1 properties object id:&quot;, id(interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;]))
print(&quot;Ethernet2 properties object id:&quot;, id(interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;]))

# Dump YAML to stdout
print(&quot;\n##### Resulting YAML:\n&quot;)
print(yaml.safe_dump(interfaces))
</code></pre>
<p>Program #1 output:</p>
<pre><code class="language-text">Ethernet1 properties object id: 41184424
Ethernet2 properties object id: 41182536

##### Resulting YAML:

Ethernet1:
  description: Uplink to core-1
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
Ethernet2:
  description: Uplink to core-2
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
</code></pre>
<p>Program #2:</p>
<pre><code class="language-python"># yaml_same_ids.py
import yaml


interfaces = dict(
    Ethernet1=dict(description=&quot;Uplink to core-1&quot;, speed=1000, mtu=9000),
    Ethernet2=dict(description=&quot;Uplink to core-2&quot;, speed=1000, mtu=9000),
)

prop_vals = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]

interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = prop_vals
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = prop_vals

# Show IDs referenced by &quot;properties&quot; key
print(&quot;Ethernet1 properties object id:&quot;, id(interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;]))
print(&quot;Ethernet2 properties object id:&quot;, id(interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;]))

# Dump YAML to stdout
print(&quot;\n##### Resulting YAML:\n&quot;)
print(yaml.safe_dump(interfaces))
</code></pre>
<p>Program #2 output:</p>
<pre><code class="language-text">Ethernet1 properties object id: 13329416
Ethernet2 properties object id: 13329416

##### Resulting YAML:

Ethernet1:
  description: Uplink to core-1
  mtu: 9000
  properties: &amp;id001
  - pim
  - ptp
  - lldp
  speed: 1000
Ethernet2:
  description: Uplink to core-2
  mtu: 9000
  properties: *id001
  speed: 1000
</code></pre>
<p>So, two pretty much identical programs, two data structures containing identical data but two different results of YAML dump.</p>
<p>What caused this difference? It’s all due to a tiny change in the way we assigned values to <code>properties</code> key:</p>
<p>Program #1</p>
<pre><code>interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]
</code></pre>
<p>Program #2</p>
<pre><code>properties = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]

interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = properties
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = properties
</code></pre>
<p>In <strong>Program #1</strong> we created two new lists and passed the references to relevant <code>properties</code> keys. These look to be the same but are actually two completely separate objects.</p>
<p>In <strong>Program #2</strong> we first created a list which was assigned to <code>prop_vals</code> variable. We then assigned <code>prop_vals</code> to each of the <code>properties</code> keys. This essentially means that each of the keys now references the same list object.</p>
<p>We also asked Python to give us IDs of the objects referenced by <code>properties</code> keys. Here we can see that indeed IDs in <strong>Program #1</strong> differ but they’re the same in <strong>Program #2</strong>:</p>
<p>Program #1 IDs:</p>
<pre><code>Ethernet1 properties object id: 41184424
Ethernet2 properties object id: 41182536
</code></pre>
<p>Program #2 IDs:</p>
<pre><code>Ethernet1 properties object id: 13329416
Ethernet2 properties object id: 13329416
</code></pre>
<p>And that’s it. That’s how PyYAML knows it should use aliases and anchors to represent first and subsequent references to the same object.</p>
<p>For completeness, here’s an example of loading YAML file with references that we just dumped:</p>
<pre><code class="language-python">import yaml


with open(&quot;yaml_files/interfaces_same_ids.yml&quot;) as fin:
    interfaces = yaml.safe_load(fin)

# Show IDs referenced by &quot;properties&quot; key
print(&quot;Ethernet1 properties object id:&quot;, id(interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;]))
print(&quot;Ethernet2 properties object id:&quot;, id(interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;]))
</code></pre>
<p>IDs of the loaded <code>properties</code> keys:</p>
<pre><code class="language-text">Ethernet1 properties object id: 19630664
Ethernet2 properties object id: 19630664
</code></pre>
<p>As you can see IDs are the same, so information about <code>properties</code> keys referencing same object was preserved.</p>
<p><a name="yaml-dump-no-refs"></a></p>
<h2 id="yamldumpdontuseanchorsandaliases">YAML dump - don’t use anchors and aliases</h2>
<p>You know know what anchors and aliases are, what they’re used for and where they come from. It’s now time to show you how to stop PyYAML from using them during dump operation.</p>
<p>PyYAML does not have built-in setting allowing disabling of the default behaviour. Fortunately there are two ways in which we can prevent references from being used:</p>
<ol>
<li>Force all data objects to have unique IDs by using <code>copy.deepcopy()</code> function</li>
<li>Override <code>ignore_aliases()</code> method in PyYAML <code>Dumper</code> class</li>
</ol>
<p>Method 1 might require source code modifications in multiple places and could be slow when copying large amounts of compound objects.</p>
<p>Method 2 only requires few lines of code to define custom dumper class. This can then be used alongside standard PyYAML dumper.</p>
<p>In any case, have a look at both and decide which one fits your case better.</p>
<p><a name="deepcopy"></a></p>
<h3 id="usingcopydeepcopyfunction">Using copy.deepcopy() function</h3>
<p>Python standard library provides us with <code>copy.deepcopy()</code> function which returns copy of an object, and copies of objects within that object if any found.</p>
<p>As we’ve seen already, PyYAML serializer uses anchors and aliases when it finds references to the same object. By applying <code>deepcopy()</code> during object assignment we’ll ensure all of these will have unique IDs. The end result? No YAML references in the final dump.</p>
<p>Program #2, modified to use <code>deepcopy()</code>:</p>
<pre><code class="language-python"># yaml_same_ids_deep_copy.py
from copy import deepcopy

import yaml


interfaces = dict(
    Ethernet1=dict(description=&quot;Uplink to core-1&quot;, speed=1000, mtu=9000),
    Ethernet2=dict(description=&quot;Uplink to core-2&quot;, speed=1000, mtu=9000),
)

prop_vals = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]

interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = deepcopy(prop_vals)
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = deepcopy(prop_vals)

# Show IDs referenced by &quot;properties&quot; key
print(&quot;Ethernet1 properties object id:&quot;, id(interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;]))
print(&quot;Ethernet2 properties object id:&quot;, id(interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;]))

# Dump YAML to stdout
print(&quot;\n##### Resulting YAML:\n&quot;)
print(yaml.safe_dump(interfaces))
</code></pre>
<p>Result:</p>
<pre><code>Ethernet1 properties object id: 19775848
Ethernet2 properties object id: 19823048

##### Resulting YAML:

Ethernet1:
  description: Uplink to core-1
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
Ethernet2:
  description: Uplink to core-2
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
</code></pre>
<p>We passed <code>prop_vals</code> to <code>deepcopy()</code> during each assignment resulting in two new copies of that data. In the output we have two different IDs even though we reused <code>prop_vals</code>. The final YAML representation has no references, which is exactly what we wanted.</p>
<p><a name="override"></a></p>
<h3 id="overridingignore_aliasesmethod">Overriding <code>ignore_aliases()</code> method</h3>
<p>To completely disable generation of YAML references we can sub-class <code>Dumper</code> class and override its <code>ignore_aliases</code> method:</p>
<p>Class definition, borrowed from Issue #103 posted on PyYAML GitHub page:</p>
<pre><code class="language-python">class NoAliasDumper(yaml.SafeDumper):
    def ignore_aliases(self, data):
        return True
</code></pre>
<p>You could also monkey-patch the actual Dumper class but I think this solution is safer and more elegant.</p>
<p>We’ll now take <code>NoAliasDumper</code> and use it to modify Program #2:</p>
<pre><code class="language-python"># yaml_same_ids_custom_dumper.py
import yaml


class NoAliasDumper(yaml.SafeDumper):
    def ignore_aliases(self, data):
        return True


interfaces = dict(
    Ethernet1=dict(description=&quot;Uplink to core-1&quot;, speed=1000, mtu=9000),
    Ethernet2=dict(description=&quot;Uplink to core-2&quot;, speed=1000, mtu=9000),
)

prop_vals = [&quot;pim&quot;, &quot;ptp&quot;, &quot;lldp&quot;]

interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;] = prop_vals
interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;] = prop_vals

# Show IDs referenced by &quot;properties&quot; key
print(&quot;Ethernet1 properties object id:&quot;, id(interfaces[&quot;Ethernet1&quot;][&quot;properties&quot;]))
print(&quot;Ethernet2 properties object id:&quot;, id(interfaces[&quot;Ethernet2&quot;][&quot;properties&quot;]))

# Dump YAML to stdout
print(&quot;\n##### Resulting YAML:\n&quot;)
print(yaml.dump(interfaces, Dumper=NoAliasDumper))
</code></pre>
<p>Output:</p>
<pre><code class="language-text">Ethernet1 properties object id: 19455080
Ethernet2 properties object id: 19455080

##### Resulting YAML:

Ethernet1:
  description: Uplink to core-1
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
Ethernet2:
  description: Uplink to core-2
  mtu: 9000
  properties:
  - pim
  - ptp
  - lldp
  speed: 1000
</code></pre>
<p>Perfect, <code>properties</code> keys reference the same object but dumped YAML no longer uses aliases and anchors. This is exactly what we needed.</p>
<p>Note that I replaced <code>yaml.safe_dump</code> with <code>yaml.dump</code> in the above example. This is because we need to explicitly pass our modified <code>Dumper</code> class. However  <code>NoAliasDumper</code> inherited from <code>yaml.SafeDumper</code> class so we still get the same protection we do when using <code>yaml.safe_dump</code>.</p>
<p><a name="conclusion"></a></p>
<h2 id="conclusion">Conclusion</h2>
<p>This brings us to the end of the post. I hope I helped you in understanding what are <code>&amp;id001</code>, <code>*id001</code> found in YAML files and where they com from. You now also know how to stop PyYAML from using anchors and aliases when serializing data structures, should you ever need it.</p>
<p><a name="references"></a></p>
<h2 id="references">References:</h2>
<ol>
<li>PyYAML GitHub repository. Issue #103 Disable Aliases/Anchors: <a href="https://github.com/yaml/PyYAMLHi/issues/103">https://github.com/yaml/PyYAMLHi/issues/103</a></li>
<li>YAML specification. Section 3.1.1. Dump: <a href="https://yaml.org/spec/1.2/spec.html#id2762313">https://yaml.org/spec/1.2/spec.html#id2762313</a></li>
<li>YAML specification. Section 6.9.2. Node Anchors. <a href="https://yaml.org/spec/1.2/spec.html#id2785586">https://yaml.org/spec/1.2/spec.html#id2785586</a></li>
<li>YAML specification. Section 7.1. Alias Nodes. <a href="https://yaml.org/spec/1.2/spec.html#id2786196">https://yaml.org/spec/1.2/spec.html#id2786196</a></li>
<li>GitHub repo with resources for this post. <a href="https://github.com/progala/ttl255.com/tree/master/yaml/anchors-and-aliases" target="_blank">https://github.com/progala/ttl255.com/tree/master/yaml/anchors-and-aliases</a></li>
</ol>
</div>]]></content:encoded></item><item><title><![CDATA[Jinja2 Tutorial - Part 3 - Whitespace control]]></title><description><![CDATA[Third post in the Jinja2 tutorial series deals with whitespace control. We learn where whitespaces come from and how to control them.]]></description><link>https://ttl255.com/jinja2-tutorial-part-3-whitespace-control/</link><guid isPermaLink="false">5ef89b821e69ff52c5b065a5</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[automation]]></category><category><![CDATA[Ansible]]></category><category><![CDATA[python]]></category><dc:creator><![CDATA[Przemek]]></dc:creator><pubDate>Sun, 28 Jun 2020 19:14:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Text documents are the final result of rendering templates. Depending on the end consumer of these documents whitespace placement could be significant. One of the major niggles in Jinja2, in my opinion, is the way control statements and other elements affect whitespace output in the end documents.</p>
<p>To put it bluntly, mastering whitespaces in Jinja2 is the only way of making sure your templates generate text exactly the way you intended.</p>
<p>Now we know the importance of the problem, time to understand where it originates, to do that we’ll have a look at a lot of examples. Then we'll learn how we can control rendering whitespaces in Jinja2 templates.</p>
<h2 id="jinja2tutorialseries">Jinja2 Tutorial series</h2>
<ul>
<li><a href="https://ttl255.com/jinja2-tutorial-part-1-introduction-and-variable-substitution/">Jinja2 Tutorial - Part 1 - Introduction and variable substitution</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-2-loops-and-conditionals/">Jinja2 Tutorial - Part 2 - Loops and conditionals</a></li>
<li>Jinja2 Tutorial - Part 3 - Whitespace control</li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-4-template-filters/">Jinja2 Tutorial - Part 4 - Template filters</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-5-macros/">Jinja2 Tutorial - Part 5 - Macros</a></li>
<li><a href="https://ttl255.com/jinja2-tutorial-part-6-include-and-import/">Jinja2 Tutorial - Part 6 - Include and Import</a></li>
<li><a href="https://ttl255.com/j2live-online-jinja2-parser/">J2Live - Online Jinja2 Parser</a></li>
</ul>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#ws-in-j2">Understanding whitespace rendering in Jinja2</a></li>
<li><a href="#or-alt">Finding origin of whitespaces - alternative way</a>
<ul>
<li><a href="#or-ex">Origin of whitespaces - examples</a></li>
</ul>
</li>
<li><a href="#ws-control">Controlling Jinja2 whitespaces</a>
<ul>
<li><a href="#trim-strip">Trimming and stripping in action</a></li>
<li><a href="#man-ctl">Manual control</a></li>
<li><a href="#block-ind">Indentation inside of Jinja2 blocks</a></li>
</ul>
</li>
<li><a href="#ws-ansible">Whitespace control in Ansible</a>
<ul>
<li><a href="#ex-pb">Example Playbooks</a></li>
</ul>
</li>
<li><a href="#thoughts">Closing thoughts</a></li>
<li><a href="#references">References</a></li>
<li><a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p3-whitespace-control" target="_blank">GitHub repository with resources for this post</a></li>
</ul>
<p><a name="ws-in-j2"></a></p>
<h2 id="understandingwhitespacerenderinginjinja2">Understanding whitespace rendering in Jinja2</h2>
<p>We'll start our learning by looking at how Jinja2 renders whitespaces by looking at trivial example, template with no variables, just two lines of text and a comment:</p>
<pre><code class="language-text">Starting line
{# Just a comment #}
Line after comment
</code></pre>
<p>This is how it looks like when it’s rendered:</p>
<pre><code class="language-text">Starting line

Line after comment
</code></pre>
<p>Ok, what happened here? Did you expect an empty line to appear in place of our comment? I did not. I’d expect the comment line to just disappear into nothingness, but that’s not the case here.</p>
<p>So here’s a very important thing about Jinja2. All of the language blocks are removed when the template is rendered but <strong>all</strong> of the whitespaces remain in place. That is if there are spaces, tabs, or newlines, before or after, blocks, then these will be rendered.</p>
<p>This explains why comment block left a blank line once template was rendered. There is a newline character after the <code>{# #}</code> block. While the block itself was removed, newline remained.</p>
<p>Below is a more involving, but fairly typical, template, containing <code>for</code> loop and <code>if</code> statements:</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() %}
interface {{ iname }}
 description {{ idata.description }}
  {% if idata.ipv4_address is defined %}
 ip address {{ idata.ipv4_address }}
  {% endif %}
{% endfor %}
</code></pre>
<p>Values that we feed into the template:</p>
<pre><code class="language-text">interfaces:
  Ethernet1:
    description: capture-port
  Ethernet2:
    description: leaf01-eth51
    ipv4_address: 10.50.0.0/31
</code></pre>
<p>And this is how Jinja2 will render this, with all settings left to defaults:</p>
<pre><code class="language-text">
interface Ethernet1
 description capture-port
  

interface Ethernet2
 description leaf01-eth51
  
 ip address 10.50.0.0/31
  

</code></pre>
<p>This doesn’t look great, does it? There are extra newlines added in few places. Also, interestingly enough, there are leading spaces on some lines, that can’t be seen on screen but could really break things for us in the future. Overall it’s difficult to figure out where all of the whitespaces came from.</p>
<p>To help you in better visualizing generated text, here’s the same output, but now with all of the whitespaces rendered:</p>
<img src="https://ttl255.com/content/images/2020/06/vis-ws-ex01.png" style="float: left; clear: both; display: block;">
<p style="clear: both;"></p><p>
</p><p>Each bullet point represents a space character, and return icon represents newlines. You should now clearly see leading spaces that were left by Jinja2 block on three of the lines, as well as all of the extra newlines.</p>
<p>Ok, that’s all great you say, but it still is not so obvious where these came from. The real question we want to answer is:</p>
<blockquote>
<p>Which template line contributed to which line in the final result?</p>
</blockquote>
<p>To answer that question I rendered whitespaces in the template as well as the output text. Then I added colored, numbered, highlight blocks in the lines of interest, to allow us to match source with the end product.</p>
<img src="https://ttl255.com/content/images/2020/12/ws-template-ex01-corr-24-12-2020.png" style="float: left; clear: both; display: block; padding: 1em;">
<img src="https://ttl255.com/content/images/2020/12/ws-render-ex01-collaps-24-12-2020.png" style="float: left; clear: both; display: block; padding: 1em;">  
<p style="clear: both;">You should now see very easily where each of the Jinja blocks adds whitespaces to the resulting text.</p>
<p>If you’re also curious <strong>why</strong> then read on for detailed explanation:</p>
<ol>
<li>
<p>Line containing <code>{% for %}</code> block, number 1 with blue outlines, ends with a newline. This block gets executed for each key in dictionary. We have 2 keys, so we get extra 2 newlines inserted into final text.</p>
</li>
<li>
<p>Line containing <code>{% if %}</code> block, numbers 2a and 2b with green and light-green outlines, has 2 leading spaces and ends with a newline. This is where things get interesting. The actual <code>{% if %}</code> block is removed leaving behind 2 spaces that always get rendered. But trailing newline is inside of the block. This means that with <code>{% if %}</code> evaluating to <code>false</code> we get 2a but NOT 2b. If it evaluates to <code>true</code> we get both 2a AND 2b.</p>
</li>
<li>
<p>Line containing <code>{% endif %}</code> block, numbers 3a and 3b with red and orange outlines, has 2 leading spaces and ends with a newline. This is again interesting and our situation here is the reverse of previous case. Two leading spaces are inside of the <code>if</code> block, but the newline is outside of it. So 3b, newline, is always rendered. But when <code>{% if %}</code> block evaluates to <code>true</code> we also get 3a, if it's <code>false</code> then we get 3b only.</p>
</li>
</ol>
<p>It’s also worth pointing out that if your template continued after <code>{% endfor %}</code> block, that block would contribute one extra newline. But worry not, we’ll have some examples later on illustrating this case.</p>
<p>I hope you’ll agree with me that the template we used in our example wasn’t especially big or complicated, yet it resulted in a fair amount of additional whitespaces.</p>
<p>Luckily, and I couldn’t stress enough how useful that is, there are ways of changing Jinja2 behavior and taking back control over exact look and feel of our text.</p>
<p><strong>Note</strong>. The above explanation was updated on 12 Dec 2020. Previously 1st occurence of 3b was incorrectly attributed to 2b. Many thanks to Lawrr who triple-checked me and greatly helped in getting to the bottom of this!</p>
<p><a name="or-alt"></a></p>
<h2 id="findingoriginofwhitespacesalternativeway">Finding origin of whitespaces - alternative way</h2>
<p>We’ve talked a bit how to tame Jinja’s engine with regards to whitespace generation. You also know that tools like <a href="https://j2live.ttl255.com"><strong>J2Live</strong></a> can help you in visualizing all the whitespaces in the produced text. But can we tell with certainty which template line, containing block, contributed these characters to the final render?</p>
<p>To get answer to that question we can use a little trick. I came up with the following technique, that doesn’t require any external tools, for matching whitespaces coming from template block lines with extraneous whitespaces appearing in the resulting text document.</p>
<p>This method is quite simple really, you just need to add unambiguous characters to each of the block lines in the template that correspond to lines in the rendered document.</p>
<p>I find it works especially well with template inheritance and macros, topics we will discuss in the upcoming parts of this tutorial.</p>
<p><a name="or-ex"></a></p>
<h3 id="originofwhitespacesexamples">Origin of whitespaces - examples</h3>
<p>Let’s see that secret sauce in action then. We’ll place additional characters, carefully selected so that they stand out from surrounding text, in strategic places on the lines with Jinja2 blocks. I’m using the same template we already worked with so previously that you can easily compare the results.</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() %}(1)
interface {{ iname }}
 description {{ idata.description }}
  (2){% if idata.ipv4_address is defined %}
 ip address {{ idata.ipv4_address }}
  (3){% endif %}
{% endfor %}
</code></pre>
<p>Final result:</p>
<pre><code class="language-text">(1)
interface Ethernet1
 description capture-port
  (2)
(1)
interface Ethernet2
 description leaf01-eth51
  (2)
 ip address 10.50.0.0/31
  (3)
</code></pre>
<p>I added <code>(1)</code>, <code>(2)</code> and <code>(3)</code> characters on the lines where we have Jinja2 blocks. The end result matches what we got back from J2Live with <code>Show whitespaces</code> option enabled.</p>
<p>If you don’t have access to <a href="https://j2live.ttl255.com"><strong>J2Live</strong></a> or you need to troubleshoot whitespace placement in production templates, then I definitely recommend using this method. It’s simple but effective.</p>
<p>Just to get more practice, I’ve added extra characters to slightly more complex template. This one has branching <code>if</code> statement and some text below final <code>endfor</code> to allow us to see what whitespaces come from that block.</p>
<p>Our template:</p>
<pre><code class="language-text">{% for acl, acl_lines in access_lists.items() %}(1)
ip access-list extended {{ acl }}
  {% for line in acl_lines %}(2)
    (3){% if line.action == &quot;remark&quot; %}
    remark {{ line.text }}
    (4){% elif line.action == &quot;permit&quot; %}
    permit {{ line.src }} {{ line.dst }}
    (5){% endif %}
  {% endfor %}(6)
{% endfor %}(7)

# All ACLs have been generated
</code></pre>
<p>Data used to render it:</p>
<pre><code class="language-text">access_lists:
  al-hq-in:
    - action: remark
      text: Allow traffic from hq to local office
    - action: permit
      src: 10.0.0.0/22
      dst: 10.100.0.0/24
</code></pre>
<p>End result:</p>
<pre><code class="language-text">(1)
ip access-list extended al-hq-in
  (2)
    (3)
    remark Allow traffic from hq to local office
    (4)
  (2)
    (3)
    permit 10.0.0.0/22 10.100.0.0/24
    (5)
  (6)
(7)

# All ACLs have been generated
</code></pre>
<p>A lot is happening here but there are no mysteries anymore. You can easily match each source line with line in the final text. And knowing where the whitespaces are coming from is the first step to learning how to control them, which is what we’re going to talk about shortly.</p>
<p>Also, for comparison is the rendered text without using <code>helper</code> characters:</p>
<pre><code class="language-text">
ip access-list extended al-hq-in
  
    
    remark Allow traffic from hq to local office
    
  
    
    permit 10.0.0.0/22 10.100.0.0/24
    
  


# All ACLs have been generated
</code></pre>
<p>If you’re still reading this, congratulations! Your dedication to mastering whitespace rendering is commendable. Good news is that we’re now getting to the bit where we learn how to control Jinja2 behavior.</p>
<p><a name="ws-control"></a></p>
<h2 id="controllingjinja2whitespaces">Controlling Jinja2 whitespaces</h2>
<p>There are broadly three ways in which we can control whitespace generation in our templates:</p>
<ol>
<li>Enable one of, or both, <code>trim_blocks</code> and <code>lstrip_blocks</code> rendering options.</li>
<li>Manually strip whitespaces by adding a minus sign <code>-</code> to the start or end of the block.</li>
<li>Apply indentation inside of Jinja2 blocks.</li>
</ol>
<p>First, I’ll give you an easy, by far more preferable, way of taming whitespace and then we’ll dig into the more involving methods.</p>
<p>So here it comes:</p>
<blockquote>
<p>Always render with <code>trim_blocks</code> and <code>lstrip_blocks</code> options enabled.</p>
</blockquote>
<p>That’s it, the big secret is out. Save yourself trouble and tell Jinja2 to apply trimming and stripping to all of the blocks.</p>
<p>If you use Jinja2 as part of another framework then you might have to consult documentation to see what the default behaviour is and how it can be changed. Later in this post I will explain how we can control whitespaces when using Ansible to render Jinja2 templates.</p>
<p>Just a few words of explanation on what these options do. Trimming removes newline after block while stripping removes all of spaces and tabs on the lines preceding the block. Now, if you enable trimming alone, you might still get some funny output if there are any leading whitespaces on the lines containing blocks, so that’s why I recommend having both of these enabled.</p>
<p><a name="trim-strip"></a></p>
<h3 id="trimmingandstrippinginaction">Trimming and stripping in action</h3>
<p>For example, this is what happens when we enable block trimming but leave block stripping disabled:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
          remark Allow traffic from hq to local office
              permit 10.0.0.0/22 10.100.0.0/24
      
# All ACLs have been generated
</code></pre>
<p>That’s the same example we just had a look at, and I’m sure you didn’t expect this to happen at all. Let’s add some extra characters to figure out what happened:</p>
<pre><code class="language-text">{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
  {% for line in acl_lines %}
    (3){% if line.action == &quot;remark&quot; %}
    remark {{ line.text }}
    (4){% elif line.action == &quot;permit&quot; %}
    permit {{ line.src }} {{ line.dst }}
    {% endif %}
  {% endfor %}
{% endfor %}

# All ACLs have been generated
</code></pre>
<pre><code class="language-text">ip access-list extended al-hq-in
      (3)    remark Allow traffic from hq to local office
    (4)      (3)    permit 10.0.0.0/22 10.100.0.0/24
      
# All ACLs have been generated
</code></pre>
<p>Another puzzle solved, we got rid of newlines with <code>trim_blocks</code> enabled but leading spaces in front of <code>if</code> and <code>elif</code> blocks remained. Something that is completely undesirable.</p>
<p>So how would this template render if we had both trimming and stripping enabled? Have a look:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
    remark Allow traffic from hq to local office
    permit 10.0.0.0/22 10.100.0.0/24

# All ACLs have been generated
</code></pre>
<p>Quite pretty right? This is what I meant when I talked about getting <strong>intended</strong> result. No surprises, no extra newlines or spaces, final text matches our expectations.</p>
<p>Now, I said enabling trim and lstrip options is an easy way, but if for whatever reason you can’t use it, or want to have total control over how whitespaces are generated on a per-block then we need to resort to manual control.</p>
<p><a name="man-ctl"></a></p>
<h3 id="manualcontrol">Manual control</h3>
<p>Jinja2 allows us to manually control generation of whitespaces. You do it by using a minus sing <code>-</code> to strip whitespaces from blocks, comments or variable expressions. You need to add it to the start or end of given expression to remove whitespaces before or after the block, respectively.</p>
<p>As always, it’s best to learn from examples. We’ll go back to example from the beginning of the post. First we render without any <code>-</code> signs added:</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() %}
interface {{ iname }}
 description {{ idata.description }}
  {% if idata.ipv4_address is defined %}
 ip address {{ idata.ipv4_address }}
  {% endif %}
{% endfor %}
</code></pre>
<p>Result:<br>
<img src="https://ttl255.com/content/images/2020/06/hyphen-01.png" style="float: left; clear: both; display: block;"></p>
<p style="clear: both"></p>
<p>Right, some extra newlines and there are additional spaces as well. Let’s add minus sign at the end of <code>for</code> block:</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 description {{ idata.description }}
  {% if idata.ipv4_address is defined %}
 ip address {{ idata.ipv4_address }}
  {% endif %}
{% endfor %}
</code></pre>
<img src="https://ttl255.com/content/images/2020/06/hyphen-02.png" style="float: left; clear: both; display: block;">
<p style="clear: both"></p>
<p>Looks promising, we removed two of the extra newlines.</p>
<p>Next we look at <code>if</code> block. We need to get rid of the newlines this block generates so we try adding <code>-</code> at the end, just like we did with <code>for</code> block.</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 description {{ idata.description }}
  {% if idata.ipv4_address is defined -%}
 ip address {{ idata.ipv4_address }}
  {% endif %}
{% endfor %}
</code></pre>
<img src="https://ttl255.com/content/images/2020/06/hyphen-03.png" style="float: left; clear: both; display: block;">
<p style="clear: both"></p>
<p>Newline after line with <code>description</code> under <code>Ethernet2</code> is gone. Oh, but wait, why do we have two spaces in the line with <code>ip address</code> now? Aha! These must’ve been the two spaces preceding the <code>if</code> block. Let’s just add <code>-</code> to the beginning of that block as well and we’re done!</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 description {{ idata.description }}
  {%- if idata.ipv4_address is defined -%}
 ip address {{ idata.ipv4_address }}
  {% endif %}
{% endfor %}
</code></pre>
<img src="https://ttl255.com/content/images/2020/06/hyphen-04.png" style="float: left; clear: both; display: block;">
<p style="clear: both"></p>
<p>Hmm, now it’s all broken! What happened here? A very good question indeed.</p>
<p>So here’s the thing. These magical minus signs remove <strong>all</strong> of the whitespaces before or after the block, not just whitespaces on the same line. Not sure if you expected that, I certainly did not when I first used manual whitespace control!</p>
<p>In our concrete case, the first <code>-</code> we added to the end of <code>if</code> block stripped newline AND one space on the next line, the one before <strong>ip address</strong>*. Because, if we now look closely, we should’ve had three spaces there not just two. One space that we placed there ourselves and two spaces that we had in front of the <code>if</code> block. But that space placed by us was removed by Jinja2 due to <code>-</code> sign placed in the <code>if</code> block.</p>
<p>Not all is lost though. You might notice that just adding <code>-</code> at the beginning of <code>if</code> and <code>endif</code> blocks will render text as intended. Let’s try doing that and see what happens.</p>
<pre><code class="language-text">{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 description {{ idata.description }}
  {%- if idata.ipv4_address is defined %}
 ip address {{ idata.ipv4_address }}
  {%- endif %}
{% endfor %}
</code></pre>
<p>Result:</p>
<img src="https://ttl255.com/content/images/2020/06/hyphen-05.png" style="float: left; clear: both; display: block;">
<p style="clear: both"></p>
<p>Bingo! We got rid of all those pesky whitespaces! But was this easy and intuitive? Not really. And to be fair, this wasn’t a very involving example. Manually controlling whitespaces is certainly possible but you must remember that all whitespaces are removed, and just the ones on the same line as the block.</p>
<p><a name="block-ind"></a></p>
<h3 id="indentationinsideofjinja2blocks">Indentation inside of Jinja2 blocks</h3>
<p>There is a method of writing blocks that makes things a bit easier and predictable. We simply put opening of the block at the beginning of the line and apply indentation inside of the block. As always, easier to explain using an example:</p>
<pre><code class="language-text">{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
{%   for line in acl_lines %}
{%     if line.action == &quot;remark&quot; %}
  remark {{ line.text }}
{%     elif line.action == &quot;permit&quot; %}
  permit {{ line.src }} {{ line.dst }}
{%     endif %}
{%   endfor %}
{% endfor %}

# All ACLs have been generated
</code></pre>
<p>As you can see we moved block opening <code>{%</code> all the way to the left and then indented as appropriate inside of the block. Jinja2 doesn’t care about extra spaces inside of the <code>if</code> or <code>for</code> blocks, it will simply ignore them. It only concerns itself with whitespaces that it finds outside of blocks.</p>
<p>Let’s render this to see what we get:</p>
<pre><code class="language-text">
ip access-list extended al-hq-in
  
    
  remark Allow traffic from hq to local office
    
  
    
  permit 10.0.0.0/22 10.100.0.0/24
    
  


# All ACLs have been generated
</code></pre>
<p>How is this any better you may ask? At first sight, not much better at all. But before I tell you why this might be a good idea, and where it is especially useful I’ll show you the same template as we had it previously.</p>
<p>We’ll render it with <code>trim_blocks</code> enabled:</p>
<pre><code class="language-text">{% for acl, acl_lines in access_lists.items() %}
ip access-list extended {{ acl }}
  {% for line in acl_lines %}
    {% if line.action == &quot;remark&quot; %}
  remark {{ line.text }}
    {% elif line.action == &quot;permit&quot; %}
  permit {{ line.src }} {{ line.dst }}
    {% endif %}
  {% endfor %}
{% endfor %}

# All ACLs have been generated
</code></pre>
<pre><code class="language-text">ip access-list extended al-hq-in
        remark Allow traffic from hq to local office
            permit 10.0.0.0/22 10.100.0.0/24
      
# All ACLs have been generated
</code></pre>
<p>Terrible, just terrible. Indentation got completely out of whack. But what am I trying to show you? Well, let’s now render version of this template with indentations inside of <code>for</code> and <code>if</code> blocks, again with <code>trim_blocks</code> turned on:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
  remark Allow traffic from hq to local office
  permit 10.0.0.0/22 10.100.0.0/24

# All ACLs have been generated
</code></pre>
<p>Isn’t that nice? Remember that previously we had to enable both <code>trim_blocks</code> and <code>lstrip_blocks</code> to achieve the same effect.</p>
<p>So here it is:</p>
<blockquote>
<p>Starting Jinja2 blocks from the beginning of the line and applying indentation inside of them is roughly equivalent to enabling <code>lstrip_block</code>.</p>
</blockquote>
<p>I say <em>roughly</em> equivalent because we don't strip anything here, we just hide extra spaces inside of blocks preventing them from being picked up at all.</p>
<p>And there is an extra bonus to using this method, it will make your Jinja2 templates used in Ansible safer. Why? Read on!</p>
<p><a name="ws-ansible"></a></p>
<h2 id="whitespacecontrolinansible">Whitespace control in Ansible</h2>
<p>As you probably already know Jinja2 templates are used quite heavily when doing network automation with Ansible. Most people will use Ansible’s <code>template</code> module to do the rendering of templates. That module by default enables <code>trim_blocks</code> option but <code>lstrip_blocks</code> is turned off and needs to be enabled manually.</p>
<p>We can assume that most users will use the <code>template</code> module with default options which means that using <strong>indentation inside of the block</strong> technique will increase safety of our templates and the rendered text.</p>
<p>For the above reasons I’d recommend applying this technique if you know your templates will be used in Ansible. You will greatly reduce risk of your templates having seemingly random whitespaces popping up in your configs and other documents.</p>
<p>I would also say that it’s not a bad idea to always stick to this way of writing your blocks if you haven’t yet mastered the arcane ways of Jinja2. There are no real downsides of writing your templates this way.</p>
<p>The only side effect here is how visually templates present themselves, with a lot of blocks templates looking “busy”. This could make it difficult to see lines of text between the blocks since these need to have indentation matching your intent.</p>
<p>Personally I always try to use indentation within blocks method in templates meant for Ansible. For other templates, when rendered with Python, I do whatever feels right in terms of readability, and I render all templates with block trimming and stripping enabled.</p>
<p><a name="ex-pb"></a></p>
<h3 id="exampleplaybooks">Example Playbooks</h3>
<p>For completeness sake, I built two short Ansible Playbooks, one uses default setting for <code>template</code> module while the other enables <code>lstrip</code> option.</p>
<p>We’ll be using the same template and data we used for testing <code>trim</code> and <code>lstrip</code> options previously.</p>
<p>Playbook using default settings, i.e. only <code>trim</code> is turned on:</p>
<pre><code class="language-text">---
- hosts: localhost
  gather_facts: no
  connection: local

  vars_files:
    - vars/access-lists.yml


  tasks:
    - name: Show vars
      debug:
        msg: &quot;{{ access_lists }}&quot;

    - name: Render config for host
      template:
        src: &quot;templates/ws-access-lists.j2&quot;
        dest: &quot;out/ws-default.cfg&quot;
</code></pre>
<p>And rendering results:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
          remark Allow traffic from hq to local office
              permit 10.0.0.0/22 10.100.0.0/24
      
# All ACLs have been generated
</code></pre>
<p>If you recall, we got exactly same result when rendering this template in Python with <code>trim</code> option enabled. Again, indentations are misaligned so we need to do better.</p>
<p>Playbook enabling <code>lstrip</code>:</p>
<pre><code class="language-text">---
- hosts: localhost
  gather_facts: no
  connection: local

  vars_files:
    - vars/access-lists.yml


  tasks:
    - name: Show vars
      debug:
        msg: &quot;{{ access_lists }}&quot;

    - name: Render config for host
      template:
        src: &quot;templates/ws-access-lists.j2&quot;
        dest: &quot;out/ws-lstrip.txt&quot;
        lstrip_blocks: yes

</code></pre>
<p>Rendered text:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
    remark Allow traffic from hq to local office
    permit 10.0.0.0/22 10.100.0.0/24

# All ACLs have been generated
</code></pre>
<p>And again, same result as when you enabled <code>trim</code> and <code>lstrip</code> when rendering Jinja2 in Python.</p>
<p>Finally, let’s run the first Playbook, with default setting, using the template with indentation inside of blocks.</p>
<p>Playbook:</p>
<pre><code class="language-text">---
- hosts: localhost
  gather_facts: no
  connection: local

  vars_files:
    - vars/access-lists.yml


  tasks:
    - name: Show vars
      debug:
        msg: &quot;{{ access_lists }}&quot;

    - name: Render config for host
      template:
        src: &quot;templates/ws-bi-access-lists.j2&quot;
        dest: &quot;out/ws-block-indent.txt&quot;
</code></pre>
<p>Result:</p>
<pre><code class="language-text">ip access-list extended al-hq-in
  remark Allow traffic from hq to local office
  permit 10.0.0.0/22 10.100.0.0/24

# All ACLs have been generated
</code></pre>
<p>So, we didn’t have to enable <code>lstrip</code> option to get the same, perfect, result. Hopefully now you can see why I recommend using indentation within blocks as the default for Ansible templates. This gives you more confidence that your templates will be rendered the way you wanted them with default settings.</p>
<p><a name="thoughts"></a></p>
<h2 id="closingthoughts">Closing thoughts</h2>
<p>When I sat down to write this post I thought I knew how whitespaces in Jinja2 work. But it turns out that some behaviour was not so clear to me. It was especially true for manual stripping with <code>-</code> sign, I keep forgetting that all of the whitespaces before/after the block are stripped, not just the ones on the line with block.</p>
<p>So my advice is this: use trimming and stripping options whenever possible and generally favour indentation within blocks over indentation outside. And spend some time learning how Jinja2 generates whitespaces, that will allow you to take full control over your templates when you need it.</p>
<p>And that's it, I hope you found this post useful and I look forward to seeing you again!</p>
<p><a name="references"></a></p>
<h2 id="references">References:</h2>
<ul>
<li>Official documentation for the latest version of Jinja2 (2.11.x). Available at: <a href="https://jinja.palletsprojects.com/en/2.11.x/">https://jinja.palletsprojects.com/en/2.11.x/</a></li>
<li>Documentation for Ansible template module: <a href="https://docs.ansible.com/ansible/latest/modules/template_module.html">https://docs.ansible.com/ansible/latest/modules/template_module.html</a></li>
<li>Jinja2 Python library at PyPi. Available at: <a href="https://pypi.org/project/Jinja2/">https://pypi.org/project/Jinja2/</a></li>
<li>GitHub repo with resources for this post. Available at: <a href="https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p3-whitespace-control" target="_blank">https://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p3-whitespace-control</a></li>
</ul>
</div>]]></content:encoded></item></channel></rss>