Juniper Automatisierung mit Ansible

Rudolph
04.07.2017 6 7:09 min

[This article is also available in English]

Vor Kurzem haben wir euch mitgeteilt, dass wir derzeit Teile unserer Netzwerk Infrastruktur erneuern. Technisch haben wir das mit Ansible und Juniper gelöst, worüber wir euch hier berichten wollen. Auf unserer Suche nach Informationen mussten wir leider feststellen, dass es noch gar nicht so viel darüber zu finden gibt – also versuchen wir nun, unseren Teil dazu beizutragen. Um der Zielgruppe gerecht zu werden, wird dieser Blogartikel bald auch in englischer Sprache erscheinen.

Die Basics – Git und Ansible

Zu Ersterem muss man eigentlich nicht viel sagen. Wer heute noch versucht, etwas komplexeres ohne Versionskontrolle zu bauen, hat eigentlich schon verloren. Ob es nun Git oder etwas anderes wird, ist weitestgehend egal. Aber warum Ansible? Wir haben uns in den letzten Jahren hier bei sipgate mit Puppet und Ansible beschäftigt – zu beidem ist also Wissen vorhanden und Juniper unterstützt ebenfalls beides. Für Puppet benötigt man Zusatzsoftware auf jedem Gerät (den Puppet Agent) und am besten auch einen Puppet Master, für Ansible dagegen Ansible selbst und zusätzliche Libraries auf allen Workstations oder einem dedizierten Management Server.

Während Puppet sich bei uns seit Jahren erfolgreich um die Konfiguration der Server Basics (Authentifizierung, Logging, Paketquellen etc.) kümmert, hat sich alles darüber liegende nach und nach in Ansible entwickelt: LDAP-, DNS-Server oder Loadbalancer, aber auch sämtliche hauseigenen Dienste, die wir zum Teil mehrmals täglich mit Hilfe von Jenkins ausrollen. Aufgrund des einfacheren Ansatzes und den gut lesbaren Rollen und Playbooks haben wir uns dann für Ansible entschieden.

OK, Ansible – und weiter?

Ansible ist modular aufgebaut – und die Anzahl der offiziellen und inoffiziellen Module steigt kontinuierlich. Vor allem seit Version 2.0 aufwärts hat der Netzwerk Bereich Fahrt aufgenommen. Neben den offiziellen Modulen für Juniper Geräte gibt es auch durch Juniper selbst gepflegte Module. Beide können nebeneinander existieren und ergänzen/überschneiden sich in ihren Funktionen. Wir benutzen bisher nur die offiziellen Module – die Annahme war, dass uns das weniger Probleme bei zukünftigen Ansible Updates bereitet. Sollten wir aber auf bestimmte Features angewiesen sein, die uns nur die Juniper-eigenen Module bieten, hätten wir uns deren Setup auch näher angeschaut.

Da wir weitestgehend mit Debian bzw. Ubuntu arbeiten hier die wenigen Schritte, um die Ansible Module an den Start zu bekommen:

apt-get install python-pip libxml2-dev libffi-dev python-dev libxslt1-dev libssl-dev
pip install junos-eznc jxmlease

Achja – Ansible benutzen wir übrigens in Version 2.2. Auf Juniper Seite wird von den Modulen netconf via SSH benutzt. Das ist also neben einem Benutzer und der Management IP das Mindeste, was man vorab auf dem Ziel konfigurieren muss:

set system services ssh 
set system services netconf ssh

Wenn man sich per SSH-Key authentifiziert, macht das den Login natürlich einfacher. In unserem Fall aber kommt Radius Authentifizierung zum Einsatz – daher muss man den Juniper Modulen Benutzername und Passwort jedesmal mitgeben. Folgendes Playbook-Gerüst fragt diese Daten interaktiv im Playbook ab und speichert sie im netconf Dictionary – wie das verwendet wird, zeigt dann der nächste Abschnitt.

- hosts: core_switches
  serial: 1
  connection: local
  gather_facts: False
  vars_prompt:
    - name: "netconf_user"
      prompt: "Netconf login user"
      default: "root"
      private: no
    - name: "netconf_password"
      prompt: "Netconf login password"
      private: yes 
  vars:
    netconf:
      host: "{{ inventory_hostname }}"
      username: "{{ netconf_user }}"
      password: "{{ netconf_password }}"
      timeout: 30

  roles:
    - role: base_setup
    - role: core_setup

Templates, Templates everywhere!

Die Stärke von Ansible liegt nicht darin, irgendwo Kommandos auszuführen oder statische Dateien von A nach B zu kopieren – natürlich wollen wir eine Konfiguration, die sich dynamisch aus den Daten des Inventorys zusammenbaut. Die offiziellen Juniper Module können aber nicht direkt mit Templates umgehen und daher haben wir uns mit folgendem Konstrukt beholfen:

 - name: generate dns configuration
   template: src=dns.j2 dest=/tmp/junos_config_deploy/{{ inventory_hostname }}/dns.conf
   changed_when: false

 - name: install dns configuration
   junos_config:
     src: /tmp/junos_config_deploy/{{ inventory_hostname }}/dns.conf
     replace: yes 
     src_format: text
     provider: "{{ netconf }}"

Das bedeutet, dass wir uns zuerst die Konfiguration lokal aus einem Template generieren und sie anschließend über das junos_config Modul auf den Switch oder Router laden. Normalerweise würde JunOS unseren Konfigurations-Schnipsel mit der vorhandenen Konfiguration mergen – das Ergebnis ist dann eher undefiniert. Daher setzen wir auf die replace Syntax, mit der man in der Konfiguration durch das Schlüsselwort replace: definieren kann, dass der folgende Teilbaum alles vorhandene schlicht ersetzt. Um bei unserem Beispiel von oben zu bleiben, sähe das Template dns.j2 dann so aus:

system {
    replace:
    name-server {
{% for ip in dns_ips %}
        {{ ip }};
{% endfor %}
    }
    host-name {{ inventory_hostname_short }};
    domain-name some.domain.here;
}

Wir haben unsere Konfiguration thematisch in viele kleinere Templates gesplittet. Bisher fahren wir mit diesem Ansatz ganz gut – für uns bringt es den Vorteil, dass wir über Ansible Tags gezielt einzelne Templates ausrollen können und bei einem Fehler auch sehr schnell sehen, welcher Teil unserer Konfiguration dafür verantwortlich ist. Und Hand aufs Herz: eine fehlgeschlagene DNS-Konfiguration beunruhigt viel weniger als ein Fehler bei der Interface oder OSPF Konfiguration. Darüber hinaus sind die einzelnen Templates auch übersichtlich und gut lesbar. Trotzdem gibt es zwei Nachteile: Es können natürlich mehrere Templates in Bereichen unterhalb von z.B. system { } arbeiten – dann darf aber keines davon das replace: Schlüsselwort vor system benutzen (ansonsten würden ja sämtliche anderen Änderungen überschrieben). Außerdem bringt jeder Aufruf des junos_config Moduls einen commit mit sich – das kann je nach Device nur wenige Sekunden oder eine gefühlte Ewigkeit dauern. Wenn ein Playbook 15 commits auslöst und man das Ganze auf 30 Switches erledigen muss, kann da schon ein wenig Zeit vergehen…

Block to the rescue!

Ansible kennt mittlerweile auch eine Art try..catch Exception Handling – das Kind wurde allerdings anders genannt und hört auf block…rescue. Folgendes Konstrukt nutzen wir in allen unseren Rollen um Fehler abzufangen und das Debuggen zu erleichtern:

- block:
  - name: remove config preparation folder
    file: path=/tmp/junos_config_deploy/{{ inventory_hostname }} state=absent
    changed_when: False
  - name: generate config preparation folder
    file: path=/tmp/junos_config_deploy/{{ inventory_hostname }} mode=0700 state=directory
    changed_when: False

 [...]
  template/junos_config Tasks
  [...]

  - name: remove config preparation folder
    file: path=/tmp/junos_config_deploy/{{ inventory_hostname }} state=absent
    changed_when: False
    tags: syslog
  rescue:
    - debug: msg="configuring the switch failed. you can find the generated configs in /tmp/junos_config_deploy/{{ inventory_hostname }}/*.conf and try yourself"
    - debug: msg="scp the file to the switch and execute 'load replace <filename>' + 'commit' in conf mode"
    - fail: msg="stopping the playbook run"

Was passiert hier? Zunächst einmal sorgen wir dafür, dass der Ordner mit Konfigurationen aus einem vorherigen Playbook Run nicht liegen geblieben ist. Anschließend legen wir genau diesen Ordner neu an und generieren dann darin sämtliche Konfiguration, um sie über das junos_config Modul auf das Gerät zu laden. Wenn alles funktioniert hat, löschen wir diesen Ordner zum Schluss wieder. Diesen ganzen Bereich fassen wir mit block ein – wenn ein Task mit einem Fehler abbricht, wird automatisch die rescue Sektion angefahren und der Anwender bekommt eine kleine Hilfe zum Debuggen. Wenn sich ein Template nicht anwenden lässt, sind die Fehlermeldungen von junos_config oft nicht hilfreich. Bei den unklaren Fällen ist es der einfachste Weg, das generierte Template selbst anzuwenden und zu schauen, ob der Switch einem dabei mehr Informationen mitteilt.

Wir hoffen, dass wir damit einen ersten Einblick in das Thema Netzwerk Automatisierung geben konnten. Bei der nächsten Gelegenheit verraten wir euch beispielsweise, wie man Facts von Juniper Geräten abholt – und was man damit anstellen kann. Stay tuned!

6 Kommentare


Daniel:

Solche Beiträge finde ich sehr interessant, gerne mehr davon. Auch wenn die Resonanz in den Kommentaren eher gering ausfallen dürfte, der geneigte Leser ist eher des stillen Typs. ;-)

antworten

Marjan:

Check!

antworten

Mechanix:

Sehr hilfreich, weiter so!

antworten

Michael Rack:

Hmmmmmm……. Netter Artikel, aber ehrlich gesagt gehe ich mal davon aus, dass Unternehmen in Eurer Größe mit der Anzahl an aktiven Netzwerkkomponenten sicherlich nicht händisch die Konfigurationen ändern, wenn sich bei euch mal ein DNS-Server ändert oder Ihr ein Renaming eurer Komponenten macht…

Auf Ansible zu setzen ist in unternehmenskritischen Anwendungen sicherlich nicht falsch, man kann sich die eigene Entwicklung eigener Try-Catch Implementierung sparen, aber das macht dann ja auch nur Sinn, wenn ich automatisiert das Problem erkenne und es beheben kann. In eurem Beispiel schickt Ihr eine Meldung in den Update-Log und müsst doch wieder selber manuell aktiv werden. Somit nutzt Ihr Ansible ja nur zu einem Bruchteil seiner Möglichkeiten.

Wir managen aktuell über 450 Endgeräte (Switche, Router, Richtfunk) über einfache Bash-Scripte, welche an den Geräten ausgerollt und gestartet werden. Auch hier haben wir die Problematik, dass wir bei Problemen manuell eingreifen müssen, haben aber kein großes Framework im Hintergrund was an der ganzen Thematik nichts ändert / besseres herbeiführt.

Aber grundsätzlich ne coole Sache und weiter so.

Vielleicht habt Ihr auch mal ein besseres Beispiel aus einer Implementierung bei Euch, was öfters genutzt und gebraucht wird. Das Beispiel mit DNS-Server und Hostname Update wird euch ja nicht täglich oder wöchentlich nützlich sein ^^

antworten

Rudolph:

Hallo Michael,

das zitierte DNS-Template sollte nur als Beispiel dienen – nachdem ein Gerät frisch aus der Packung eine IP Adresse und einen Benutzer erhalten hat, wird die gesamte Konfiguration per Ansible erledigt (wir machen gar nichts mehr manuell).
Das schließt neben statischen Themen wie DNS, Authentifizierung oder ACLs auch dynamischere wie Interfaces und Routing (OSPF, BGP etc.) ein. Wenn also ein neuer Server oder ein BGP-Peer dazu kommt, wird nur das Inventory aktualisiert und das Playbook laufen gelassen (mit allen zugehörigen Templates). Wir nutzen hier Ansible nicht, um einzelne „One-Shot“ Tasks zu vereinfachen, sondern immer den Zustand der gesamten Konfiguration aller Geräte sicherzustellen.
Durch verschachtelte Ansible Gruppen (z.B. Standorte, Geräte-Typen) kann man mit dem –limit Parameter von Ansible einschränken, ob man alles, einiges oder nur bestimmte Router/Switches konfigurieren will. Wenn man darüber hinaus noch mit Tags in den Rollen arbeitet, kann man auch gezielt einzelne Templates ausrollen – dazu werden wir aber in einem weiterem Blog Post sicherlich noch mehr Informationen geben.

Über das Inventory hat man darüber hinaus immer eine auf die wesentlichen Informationen reduzierte Ansicht der Konfiguration, wenn man einmal etwas nachgucken muss.

Grüße,
Rudi

antworten

hosi:

check :-)

antworten

Schreibe einen Kommentar zu Rudolph Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert