Juniper Automatisierung mit NetBox und Jinja2

Kleine Teams müssen immer öfter große Infrastrukturen verwalten. Automatisierung macht es möglich. In diesem Blogartikel zeigen wir euch, wie das Open-Source-Tool Netbox Hilfe leisten kann.

Warum braucht man das?

Während oft beim Start einer IT-Abteilung noch jeder Switch und Router manuell konfiguriert wird und das auch okay funktionieren kann, ist diese Option nicht effektiv, wenn das System größer wird.

Hier gibt es einige fertige Lösungen am Markt, zum Beispiel Aristas CloudVision oder Juniper Apstra. Diese bieten einige Vorteile, insbesondere wenn man nicht die Teamstärke hat, um eine eigene Automatisierung zu implementieren, und bringen neben Konfigurationsmanagement auch vereinfachte Provisionierung, automatische Updates und eine Low-Code-GUI mit sich. Ein Nachteil dabei ist aber, dass nicht immer jede Topologie oder Netzwerk abgebildet werden kann, sowie dass man eine externe Abhängigkeit aufbaut und sich eventuell in einen Vendor Lock-In begibt. Gleichzeitig sind diese Lösungen verhältnismäßig einfach zu bedienen und benötigen weniger Engineers, um betrieben zu werden. Eine Kombination von eigenen Templates und Scripting ist etwa ein festes Feature von Arista CloudVision.

Während wir als Juniper- und Arista-Partner auch gerne bei diesen Lösungen beraten, Demos bereitstellen und ihr Set-up unterstützen, soll hier ein alternativer, open-source-Weg gezeigt werden.

NetBox zur Rettung

Das OpenSource-Tool “NetBox” ist eine Plattform, auf der man seine gesamte Netzwerklandschaft dokumentieren kann – und macht die Infrastruktur sowohl für neue Kollegen zugänglicher, als auch für die Kollegen aus der Compliance transparenter und dokumentiert. Während NetBox den SOLL-Zustand dokumentieren kann, ist hier noch keine fertige Logik enthalten, die Konfiguration zu generieren oder anzuwenden.

Von Beginn an haben NetBox’ Informationsstand und Gerätekonfiguration erst mal keine automatischen Zusammenhänge. Alles muss manuell gepflegt werden – doch auch hier schafft NetBox Abhilfe, denn es bringt nicht nur eine gut dokumentierte API mit sich, sondern auch ein Config Templating System, um Gerätekonfiguration zu erzeugen.

Golden Configuration

Das Stichwort #GoldenConfiguration fällt hierbei. Die “Golden Config” stellt dabei den absoluten SOLL-Zustand deines Systems dar.
Der Vergleich mit dem IST-Stand des Systems ist hier für viele Parteien interessant:

  • das IT-Team, das so schneller Fehlkonfigurationen und Probleme erkennen kann,
  • die Geschäftsführung, da Änderungen so dokumentiert und revisionssicher gespeichert werden können,
  • die Compliance-Manager, die für Audits schnell und einfach (GUI) die Daten in der Netbox abfragen können.

But how… ? Werd mal konkret!11elf

NetBox hat eine Templatingfunktion. Das heißt, du kannst ein generisches Template definieren, was dann um die gerätespezifischen Fakten angereichert wird, sodass eine fertige und einlesbare Konfigurationsdatei entsteht.

Da NetBox auf python basiert, wird hier auch die Python-basierte Sprache jinja2 benutzt. Variablen werden so {{ variable_name }} angegeben. Das reicht schon für unser erstes Template: Wir legen ein Config Template an mit folgendem Inhalt:

# Configuration for device {{ device.name }}

Weist man dieses dann einem Gerät zu, wird folgender Text generiert:

Komplizierter werden …

Jedes Gerät hat unterschiedlich viele Interfaces, also anstatt alle selbst aufzulisten gibt uns Jinja2 auch for und if-Statements:

# System Interfaces Configuration for {{device.name}}

interfaces {
    {% for interface in device.interfaces.all() %}
    {{ interface.name }} {
        description "{{ interface.label }}";
        {% if interface.enabled %}
        unit 0 {

        }
        {% else %}
        disable;
        {% endif %}
    }
    {% endfor %}
}

Wird zu:

# System Interfaces Configuration for switch-01

interfaces {
    xe-0/0/0 {
        description "fizzbuzz";
        unit 0 {

        }
    }
    xe-0/0/1 {
        description "server-offline";
        disable;
    }
}

Das sind weitestgehend schon alle Tools, die man benötigt, um eine vollständige Interface-Konfiguration vorzunehmen. Alle Informationen wie IPs, VRRP, VLANs, LACP können in der NetBox hinterlegt werden.

Fazit

So viel Aufwand nur für die Interfaces? Den Teil “Interfaces” haben wir schon übernommen! Langfristig kann sich das aber durchaus lohnen. In jedem Fall ist es gut, sich mit den verschiedenen Möglichkeiten auseinandergesetzt zu haben – letztendlich kann auch ein Mix aus mehreren Lösungen das Ideal für die eigene Infrastruktur darstellen. So bringt auch Juniper Apstra die Option mit, eigene eine Konfiguration per Jinja2 zu templaten, aber eben leider noch nicht alle Möglichkeiten, die eigene Infrastruktur wie gewünscht darzustellen oder aufzubauen.

Alles zusammengebaut:

Im Folgenden also ein weiter ausgebautes Beispiel eines Juniper Jinja2 Templates für NetBox. Es werden alle physikalischen, virtuellen und aggregierten Interfaces angelegt sowie IPs konfiguriert und VRRP Gruppen gesetzt.

# System Interfaces Configuration for {{device.name}}

{% set vars = {'vrrp': False} %}

{% macro printIps(iface, ips) %}
{% set fhrps = iface.fhrp_group_assignments.all() %}
family inet {
    {% for ip in ips %}
    {% for fhrp in fhrps %}
        {% set vips = fhrp.group.ip_addresses.all() %}
        {% if vars.update({'vrrp': fhrp if vips and vips[0].network == ip.network else False}) %} {% endif %}
    {% endfor %}
    {% if vars.vrrp %}
    {% set vrrp = vars.vrrp %}
    address {{ ip }} {
        vrrp-group {{ vrrp.group.id }} {
            virtual-address {{ vrrp.group.ip_addresses.get().address.ip }};
            priority {{ vrrp.priority }};
        }
    }
    {% if vars.update({'vrrp': False}) %} {% endif %}
    {% else %}
    address {{ ip }};
    {% endif %}
    {% endfor %}
}
{% endmacro %}

interfaces {
    {% for iface in device.interfaces.all() %}
    {{ iface.name }} {
        description "{{ iface.label }}";
        {% if iface.enabled %}
        {% if iface.lag %}
        ether-options {
            802.3ad {{ iface.lag.name }};
        }
        {% else %}
        {% set ipv4 = iface.ip_addresses.filter(address__net_contained="0/0") %}
        {% set ipv6 = iface.ip_addresses.filter(address__net_contained="::/0") %}
        unit 0 {
        {% if ipv4 or ipv6 %}
            {% if ipv4 %}
            {{ printIps(iface, ipv4)|indent(12, False) }}
            {% endif %}
            {% if ipv6 %}
            {{ printIps(iface, ipv6)|indent(12, False) }}
            {% endif %}
        {% elif iface.untagged_vlan or iface.tagged_vlans.count() %}
            family ethernet_switching {
                {% if iface.untagged_vlan %}
                interface-mode access;
                members {{ iface.untagged_vlan.name }};
                {% else %}
                interface-mode trunk;
                members [ {{ iface.tagged_vlans.all() | map(attribute='name') | join(' ') }} ];
                {% endif %}
            }
        {% endif %}
        }
        {% endif %}
        {% else %}
        disable;
        {% endif %}
    }
    {% endfor %}
}

Wird zu:

# System Interfaces Configuration for switch-01

interfaces {
    xe-0/0/0 {
        description "management";
        unit 0 {
            family ethernet_switching {
                interface-mode access;
                members SERVER;
            }
        }
    }
    xe-0/0/1 {
        description "SERVER-eth0";
        ether-options {
            802.3ad ae0;
        }
    }
    xe-0/0/2 {
        description "SERVER-eth1";
        ether-options {
            802.3ad ae0;
        }
    }
    ae0 {
        description "SERVER-LAG-20G";
        unit 0 {
            family ethernet_switching {
                interface-mode trunk;
                members [ SERVER CUSTOMER ];
            }
        }
    }
    irb.10 {
        description "MANAGEMENT";
        unit 0 {
            family inet {
                address 192.168.0.1/24;
            }
        }
    }
    irb.20 {
        description "SERVER";
        unit 0 {
            family inet {
                address 10.0.0.245/24 {
                    vrrp-group 1 {
                        virtual-address 10.0.0.1;
                        priority 1;
                    }
                }
            }
        }
    }
    irb.30 {
        description "CUSTOMER";
        unit 0 {
        }
    }
}

Einige Notizen hierzu:

  • In Jinja2 können keine Variablen außerhalb der Schleife geändert werden, daher das vars-dict.
  • In der NetBox kann eine FHRP-Gruppe mehrere VIPs haben, in Juniper nur eine. Daher wählen wir hier fix die erste.
  • Falls die Whitespaces verrückt aussehen, hilft es folgende Parameter an das Jinja2 Environment mitzugeben (in den Config Template Optionen):
    {‘trim_blocks’: ‘true’, ‘lstrip_blocks’: ‘true’}