Γενικά

Ο σκοπός του παρόντος άρθρου είναι να φέρει τον αναγνώστη σε μια πρώτη επαφή με τη Jinja2 και τις εφαρμογές της.

Τι είναι η Jinja2

Η Jinja2 είναι γλώσσα για μια γρήγορη, εκφραστική και επεκτάσιμη μηχανή προτύπων (templating engine) γραμμένη σε καθαρό python. Τα ειδικά σύμβολα θέσης στο πρότυπο επιτρέπουν την σύνταξη κώδικα παρόμοιου με την σύνταξη της Python και χρησιμοποιείται για την δημιουργία HTML,XML και άλλων γλωσσών σήμανσης (markup language).

Η πρώτη δημόσια έκδοση της Jinja έγινε το 2008, και από τότε έχουν γίνει αρκετές ενημερώσεις. Η δημιουργία της Jinja2 έχει εμπνευστεί από την built-in μηχανή προτύπων της Django , και έχει δανειστεί έννοιες και από άλλες μηχανές προτύπων (όπως JSPs). Είναι ευρέως διαδεδομένη και χρησιμοποιείται πολύ από το community της Python αλλά και στους αυτοματισμούς μέσω Ansible.

Γιατί να τη χρησιμοποιήσω / Γιατί πρέπει να με ενδιαφέρει;

Πολλά Web Frameworks (όπως Flask & Django) αλλά και Automation Frameworks (όπως Ansible & Salt) υποστηρίζουν εγγενώς τη Jinja2. Επίσης αν γνωρίζετε τη Jinja2 τότε θα είναι αρκετά εύκολο να καταλάβετε και άλλες templating languages όπως τη VTL (υποστηρίζεται σε εργαλεία διαχείρισης όπως τα Cisco Prime Infrastructure & DNA Center) με πολύ σημαντικές εφαρμογές!

Κάποιες από τις δυνατότητες της Jinja είναι :

  • Κληρονομικότητα και συμπερίληψη προτύπων.
  • Ορισμό και εισαγωγή μακροεντολών σε πρότυπα.
  • Τα πρότυπα HTML μπορούν να χρησιμοποιήσουν autoescaping για να αποτρέψουν την χρήση XSS (Cross Site Scripting) από αναξιόπιστες εισαγωγές από χρήστες.
  • Περιβάλλον sandbox, ώστε να γίνει η απόδοση των προτύπων τα οποία θεωρούνται αναξιόπιστα με ασφαλή τρόπο.
  • Υποστήριξη async για την δημιουργία προτύπων που χειρίζονται αυτόματές λειτουργείες sync/async χωρίς επιπλέον σύνταξη.
  • Τα πρότυπα μεταγλωττίζονται σε βελτιστοποιημένο κώδικα έγκαιρα και αποθηκεύονται προσωρινά(cached),ή μπορούν να μεταγλωττιστούν εκ των προτέρων.
  • Εφαρμογή εξαιρέσεων οι οποίες υποδεικνύουν την σωστή γραμμή στα πρότυπα για να διευκολύνουν τον εντοπισμό σφαλμάτων.
  • Επεκτάσιμα φίλτρα, δοκιμές, συναρτήσεις αλλά και σύνταξη.
  • Υποστήριξη δομών ελέγχου (loops & conditionals).
  • Εισαγωγή δεδομένων από εξωτερικές πηγές.

Jinja2 και Automation

Στον τομέα του αυτοματισμού όπου ένα περιβάλλον περιλαμβάνει πολλούς server/δικτυακές συσκευές όπου οι ρυθμίσεις είναι διαφορετικές, η δημιουργία στατικών αρχείων ρυθμίσεων για κάθε ένα από αυτά τα στοιχεία, μπορεί να αποδειχθεί ανιαρή και επαναλαμβανόμενη, με αποτέλεσμα την «σπατάλη» πολύτιμου χρόνου και ενέργειας. Επίσης η προτυποποίηση των ενεργειών και των σχετικών ρυθμίσεων είναι επιθυμητή ώστε να αποφεύγονται ενδεχόμενα σφάλματα που προκαλούνται από ανθρώπινα λάθη κατά την επανάληψη.

Το αποτέλεσμα της κλήσης της jinja2 engine με βάση το template και τα data μπορεί να είναι HTML, JSON, XML, ή οτιδήποτε χρησιμοποιεί καθαρό κείμενο για κωδικοποίηση, συνεπώς ακόμη και κείμενο network configuration (π.χ. Cisco IOS ή οτιδήποτε παρόμοιο).

Σημαντικό είναι να αναφέρουμε ότι ειδικά σε συνδυασμό με WEB Frameworks, η Jinja2 μπορεί να παίξει πολύ επιτυχημένα το ρόλο του Template Engine στο archicture MTV - Model/Template/View (επίσης γνωστό ως MVC - Model, View, Controller), το οποίο είναι πολύ σημαντικό να κατανοήσει όποιος θέλει να ασχοληθεί με NetDevOps & Network Programmability, καθώς χρησιμοποιείται ευρέως. Ο συσχετισμός είναι αυτονότητος αν δει κανείς τη χρήση της Jinja2 με το Flask framework, το οποίο δεν επιβάλει βαριά δομή στον προγραμματιστή.

Απλές χρήσεις

Αντικατάσταση μεταβλητών

Τα πρότυπα Jinja2 είναι απλά αρχεία προτύπων (templates) που αποθηκεύουν μεταβλητές που μπορούν να αλλάζουν από καιρό σε καιρό. Τα πρότυπα αυτά γίνονται rendered με τη χρήση των μεταβλητών και παράγουν τελικές μορφές κειμένου οι οποίες χρησιμοποιούνται στις ρυθμίσεις. Οι μεταβλητές μπορεί να είναι απλές ή σύνθετες π.χ. dictionaries, ή ακόμα και να προέρχονται από εξωτερικές πηγές, με ή χωρίς χρήση φίλτρων.

Αντικατάσταση υπό συνθήκη και Επαναλήψεις

Πέραν της αντικατάστασης των τιμών των μεταβλητών, η Jinja2 υποστηρίζει δομές ελέγχου για την υπό συνθήκη χρήση συγκεκριμένων κομματιών των προτύπων ανάλογα με την τιμή μεταβλητών ή ακόμα και επανάληψης για αντικατάσταση ομάδων μεταβλητών και επανάληψης των σχετικών τμημάτων κειμένου στις ρυθμίσεις με τις τιμές των μεταβλητών, χωρίς να χρειαστεί να γίνει αυτή η επανάληψη στο “εργαλείο” που καλεί την Jinja (π.χ. μια γλώσσα προγραμματισμού όπως η Python) Οι δομές ελέγχου κλείνονται μέσα σε blocks των χαρακτήρων {% και %}

Συνδυασμός με Python

H Jinja2 engine είναι διαθέσιμη ως πακέτο Python μέσω του Pypi: https://pypi.org/project/Jinja2/ Στο σύνδεσμο https://palletsprojects.com/p/jinja/ βρίσκει κανείς το σχετικό homesite και σύνδεσμο για την τεκμηρίωση https://jinja.palletsprojects.com/en/3.0.x/. Ίσως το πιο συχνό στοιχείο χρήσης της βιβλιοθήκης jinja2 είναι το template (ορισμός προτύπου) και η render function (παραγωγή αποτελέσματος). Μέσω της χρήσης της Jinja2 εντός Python προκύπτουν πολύ ενδιαφέροντες συνδυασμοί λόγω της ευρείας χρήσης της Python, ειδικά όταν γίνεται συνδυασμός βιβλιοθηκών και APIs, δομών ελέγχου και δομών δεδομένων, που είναι από τα μεγάλα πλεονεκτήματα της συγκεκριμένης γλώσσας. Η χρήση των προτύπων της jinja2 και του jinja2 engine σε αυτές τις περιπτώσεις ολοκληρώνουν μια παλέτα από εξαιρετικά εργαλεία για παραγωγή καθαρού και ευέλικτου κώδικα σε μεγάλο πεδίο εφαρμογών.

Υπάρχουν ακόμη όμως και πολύ ειδικές περιπτώσεις συνδυασμών, όπως η χρήση Ansible jinja2 filters στην python: https://gist.github.com/ktbyers/bdba984447636d5ac4e3d93011a861ad Περισσότερα στα παραδείγματα για Python πιο κάτω.

Συνδυασμός με Ansible

To Ansible χρησιμοποιεί τη Jinja2 για να δώσει τη δυνατότητα για δυναμικές εκφράσεις και πρόσβαση στις μεταβλητές του (variables) και τα δεδομένα που παράγονται (facts). Μπορούμε να χρησιμοποιήσουμε το template module για να εφαρμόσουμε πρότυπα στο Ansible Configuration, να χρησιμοποιήσουμε απευθείας templating σε playbooks για task names κλπ, μπορούμε να χρησιμοποιήσουμε standard filters και tests της Jinja2. Μπορούμε τέλος να χρησιμοποιήσουμε lookup plugins για να ανακτήσουμε δεδομένα από εξωτερικές πηγές όπως αρχεία, APIs, βάσεις δεδομένων κλπ για χρήση στα templates https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html .

H πιο απλή χρήση είναι η αντικατάσταση μεταβλητών. Όταν εκτελούνται τα Playbooks, αυτές οι μεταβλητές αντικαθίστανται από πραγματικές τιμές, οι πηγές των οποίων ορίζονται δυναμικά. Με αυτόν τον τρόπο, το πρότυπο προσφέρει μια αποτελεσματική και ευέλικτη λύση για τη δημιουργία ή την προσαρμογή του αρχείου διαμόρφωσης σε διαφορετικά δεδομένα εισόδου με ευκολία.

Προφανώς η υποστήριξη και μόνο της Jinja2 εγγενώς στο Ansible, δίνει ήδη μεγάλες δυνατότητες. Ειδικά για το Network Automation, έχει ιδιαίτερο ενδιαφέρον ο συνδυασμός των εξειδικευμένων Ansible modules για network operations & configuration με τη Jinja2, τόσο για τις αντικαταστάσεις τιμών δεδομένων, όσο και για την εισαγωγή από άλλες πηγές, όσο και τα jinja2 filters. Ωστόσο όσο αυξάνεται η πολυπλοκότητα των συνδυασμών, γίνεται προφανές ότι ίσως άλλες λύσεις (π.χ. Jinja2 with Python Vs Ansible) είναι προτιμότερες για network automation, όταν χρειάζεται εφαρμογή πολύπλοκης λογικής και εξειδικευμένους τρόπους χειρισμού δεδομένων από και προς το “δίκτυο”. Περισσότερα παρακάτω στο ξεχωριστό τμήμα του κειμένου για τα παραδείγματα.

Συνδυασμός με HTML/CSS & Web Frameworks

Η χρήση της Jinja2, όσο αφορά τον κώδικα που εμπλέκεται σε Web Sites, έχει προφανώς να κάνει αρχικά με την αντικατάσταση κειμένου και προεκτείνεται με την ενσωμάτωση λογικών δομών στο πρότυπο. Τα οφέλη δεν περιορίζονται σε απλές δυναμικές σελίδες HTML αλλά ακόμα και στη δυναμική διαμόρφωση και τροφοδοσία REST APIs. Το παρακάτω παράδειγμα δείχνει πως η Jinja2 μπορεί να διαμορφώσει τον τελικό HTML κώδικα μιας σελίδας (Τα επόμενα παραδείγματα αποτελούν αναδημοσίευση από https://www.codecademy.com/learn/learn-flask/)

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

    {# a comment #}
</body>
</html>

Στην περίπτωση των web frameworks που ενσωματώνουν υποστήριξη για τη Jinja2, τα templates αναδεικνύονται σε μέρος του tool ecosystem και μεγιστοποιούν το κέρδος από τη χρήση τους. Ειδικά στην περίπτωση του Flask, η Jinja2 είναι η εξ ορισμού templating engine για αυτό. Η χρήση της μπορεί να είναι εξαιρετικά απλή, π.χ.

@app.route('/')
def index():
    return render_template("index.html")

ή με παραμέτρους (realpython.com/primer-on-jinja-templating/):

@app.route("/")
def templ1():
    return render_template('template.html', my_string="This is my list:", my_list=[0,1,2,3,4,5])

Το πρότυπο μπορεί να χρησιμοποιεί φίλτρα, π.χ.

<!-- Capitalize the string -->
<h1>{{ heading_var | upper  }}</h1>
 
<!-- Default string when 
no_var not declared -->
<h1>{{ no_var | default("My Website") }}</h1>

ή δομές ελέγχου όπως if-then-else για να επιλεχθούν οι τιμές που θα αντικαταστήσουν τα πρότυπα στον κώδικα HTML, π.χ.

<html>
  <body>
    <h1>
      {% if template_feel == 'happy' %}
        This Page Is Happy
      {% elif template_feel == 'sad' %}
        This Page Is Sad
      {% else %}
        This Page Is Not Sure How It Feels
      {% endfor %}
    </h1>
  </body>
</html>

ή επαναληπτικές δομές for, όπως για την δημιουργία λιστών, π.χ.

<ul>
  {% for content_item in content_list %}
    <li>content_item</li>
  {% endfor %}
</ul>

Μπορεί ακόμα να ενσωματώνονται εντελώς στον κώδικα Flask και να υποστηρίζουν εξειδικευμένα αντικείμενα, όπως το url_for, το οποίο επιτρέπει την αποφυγή των hardcoded links και άρα την διατήρηση της ανεξαρτησίας μεταξύ των στοιχείων της αρχιτεκτονικής MTV, π.χ.

<a href="{{ url_for('index') }}">Link To Index</a>
 
<a href="{{ url_for('another_route', route_var=some_template_var) }}">Another Route With Variable</a>

Συνδυασμός με Python για Network Configuration

Πρόκειται για ειδικό συνδυασμό, καθώς το αποτέλεσμα είναι το επιθυμητό network configuration, η παραγωγή του οποίου μπορεί απλά να γίνει πιο εύκολη με τη χρήση της Jinja, ή ακόμα και να δώσει τη δυνατότητα για τη χρήση custom models ανεξάρτητων από τον κώδικα, χωρίς τη χρήση YANG Data Models. Φυσικά, η βασική λογική της χρήσης της Jinja2 στο Python δεν αλλάζει απλά επειδή αλλάζει ο σκοπός ή η εφαρμογή, αλλά και πάλι οι δυνατότητες συνδυασμών είναι τέτοιες που οδηγούν σε ενδιαφέροντα αποτελέσματα. Περισσότερα παρακάτω στο ξεχωριστό τμήμα του κειμένου για τα παραδείγματα.

Εγκατάσταση Jinja2

Η εγκατάσταση/ενημέρωση της Jinja2 μέσω python package είναι πολύ απλή και μπορεί να γίνει με την χρήση του pip:

$ pip install -U jinja2

Όπως αναφέρουν συχνά άρθρα που παρουσιάζουν την Jinja2, η κατανόηση γίνεται πιο εύκολη παρατηρώντας παραδείγματα.

Παραδείγματα

(Τα επόμενα παραδείγματα αναδημοσιεύονται από το https://realpython.com/primer-on-jinja-templating/)

Παράδειγμα με Python (1)

>>> from jinja2 import Template
>>> t = Template("Hello {{ something }}!")
>>> t.render(something="World")
'Hello World!'

Παράδειγμα με Python (2)

>>> t = Template("My favorite numbers: {% for n in range(1,10) %}{{n}} " "{% endfor %}")
>>> t.render()
'My favorite numbers: 1 2 3 4 5 6 7 8 9 '

Παράδειγμα Network Automation με Ansible

(Τα επόμενα παραδείγματα αναδημοσιεύονται από το ansible.github.io) Στο επόμενο παράδειγμα θα χρησιμοποιήσουμε Ansible Variables για να αποθηκεύσουμε τις IP διευθύνσεις δύο router. Θα χρησιμοποιήσουμε επίσης το Jinja2 lookup plugin , το οποίο θα μας μας επιτρέψει να δημιουργήσουμε ένα πρότυπο διαμόρφωσης συσκευής. Δεν αναφέρονται πλήρη playbook και κώδικας, τα οποία είναι διαθέσιμα στις πηγές του άρθρου αλλά παρουσιάζεται και επεξηγείται η λογική.

Θα χρησιμοποιήσουμε το παρακάτω IP schema για να δηλώσουμε IP διευθύνσεις στην Loopback1 στο rtr1 και rtr2 :

Device Lo1 IP address
r1 192.168.1.1/32
r2 192.168.1.2/32

Οι μεταβλητές μπορούν δηλωθούν στον φάκελο host_vars η στο group_vars. Tα interfaces αλλά και οι IP πρέπει να αποθηκευτούν ως μεταβλητές. Tο περιεχόμενου του YAML αρχείου θα δημιουργηθεί ώς εξής:

nodes:
    r1:
        Loopback1: "192.168.1.1"
    r2:
        Loopback1: "192.168.1.2"

Το παραπάνω είναι στην ουσία μια δομή nested dictionary με εσωτερικά keys τα ονόματα των interfaces και values τις ip addresses που θέλουμε να ορίσουμε.

To αρχείου template.j2 έχει τα παρακάτω περιεχόμενα:

{% for interface,ip in nodes[inventory_hostname].items() %}
interface {{interface}}
  ip address {{ip}} 255.255.255.255
{% endfor %}

Ο παραπάνω κώδικας εξηγείται ως εξής.

  • Ο κώδικας για να ορίσουμε τη δομή ελέγχου για το Jinja πρότυπο αρχίζει με {% και τελειώνει με %}. Γίνεται reference του dictionary με όνομα nodes και χρησιμοποιούνται ως key,value pairs τα inventory_hostnames σαν keys και τα σχετικά values. Ορίζεται στο loop ότι το ρόλο του key θα παίζει η μεταβλητή interface και το ρόλο του value η μεταβλητή ip.
  • Το nodes[inventory_hostname] προκαλεί την αναζήτηση του dictionary στο αρχείο group_vars/all.yml (default). Το όνομα inventory_hostname είναι το όνομα του hostname όπως αυτό έχει οριστεί στο host file της Ansible. Όταν το playbook εκτελεστεί για τον host r1 το inventory_hostname θα πάρει την τιμή r1, και αντιστοίχως για το r2.
  • Το στοιχείο items() επιστρέφει μια λίστα με λεξικά(dictionaries). Στην περίπτωση του παραδείγματος μας επιστρέφει τα κλειδιά(keys) του interface name (πχ Loopback1) και την τιμή(value) της IP address (πχ 192.168.1.1)
  • Οι μεταβλητές αποδίδονται μέσα σε άγκιστρα { } . Αυτές οι δύο μεταβλητές ορίζονται και «υπάρχουν» μόνο μέσα στην for loop. Κάθε επανάληψη της for loop θα κάνει εκ νέου ανάθεση των τιμών των μεταβλητών.
  • To loop τελειώνει με το {% endfor %}

Θα δημιουργήσουμε το Ansible playbook με όνομα config.yml με το παρακάτω περιεχόμενο:

---
- name: configure network devices
  hosts: r1,r2
  gather_facts: false
  tasks:
    - name: configure device with config
      cli_config:
        config: "{{ lookup('template', 'template.j2') }}"

Το κομμάτι που αφορά το Jinja2 είναι το τελευταίο στο οποίο ορίζονται δύο παράμετροι. Το plugin type με όνομα template και το template name το οποίο είναι το αρχείο template που δημιουργήσαμε προηγουμένως template.j2.

Αν εκτελέσουμε το Ansible Playbook

$ ansible-playbook config.yml

το αποτέλεσμα φαίνεται παρακάτω :

PLAY [rtr1,rtr2] ********************************************************************************
	 
TASK [configure device with config] ********************************************************************************
changed: [rtr1]
changed: [rtr2]
	 
PLAY RECAP ********************************************************************************
rtr1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
rtr2                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Παράδειγμα για Network Automation με Python

Αντικατάσταση μεταβλητών

(Τα επόμενα παραδείγματα αναδημοσιεύονται από το blog του Przemek Rogala https://ttl255.com/) Ας πούμε ότι θέλουμε να παράγουμε ένα κομμάτι configuration για ένα device που είναι διαχειρίσιμο μέσω CLI, και καθαρού text. Στην περίπτωση των παραδειγμάτων παρακάτω, τα devices είναι Cisco Network Devices και ο τρόπος μεταφοράς του configuration υποθετικά είναι το netmiko, ωστόσο θα μπορούσε καθένα από αυτά τα συστατικά (vendor, CLI provider) να διαφέρει, χωρίς να αλλάζει το βασικό νόημα. Έστω ότι ο στόχος για το configuration text είναι το παρακάτω, με το ζητούμενο να είναι να μπορούμε να αλλάξουμε τις παραμέτρους (ntp server ip addresses) χωρίς να αλλάξουμε το υπόλοιπο πρότυπο.

ntp server 10.1.1.20 prefer
ntp server 10.1.1.21

To template θα μπορούσε να είναι απλά κάπως έτσι:

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

Ένα python script που θα χρησιμοποιήσει το template θα μπορούσε να είναι το παρακάτω:

from jinja2 import Template

template = """
ntp server {{ ntp_server_pri }} prefer
ntp server {{ ntp_server_sec }}
"""

data = {
    "ntp_server_pri": "10.1.1.20",
    "ntp_server_sec": "10.1.1.21",
}

j2_template = Template(template)
print(j2_template.render(data))

Χρήση μεταβλητών από dictionaries

Έστω ότι παρόμοια με την προηγούμενη περίπτωση θέλουμε να χρησιμοποιήσουμε dictionaries για τα δεδομένα όπως παρακάτω για τη διαμόρφωση interfaces:

{
  "interface": {
    "name": "GigabitEthernet3/15",
    "ip_address": "10.200.10.1/24",
    "description": "Server1",
    "speed": "1000",
    "duplex": "full",
  }
}

Το πρότυπο σε αυτή την περίπτωση θα μπορούσε να είναι όπως παρακάτω.

interface {{ interface.name }}
 description {{ interface.description }}
 ip address {{ interface.ip_address }}
 speed {{ interface.speed }}
 duplex {{ interface.duplex }}

Προφανώς ισχύουν όσα είπαμε για τους συνδυασμούς που προκύπτουν, όπως και για το Ansible, αλλά στην περίπτωση του Python, όπως ήδη είπαμε, οι δυνατότητες συνδυασμών για custom logic και για επεξεργασία δεδομένων (import, process, export) είναι πολύ μεγαλύτερες.

Reading Material

Στην τέχνη του Jinjutsu (δεν υπάρχει, αστειευόμαστε), δύο αληθινοί masters είναι ο Przemek Rogala και o John Cappobianco. Ο πρώτος έχει γράψει μια καταπληκτική σειρά από άρθρα για χρήση της jinja2 σε network automation με Python (https://ttl255.com/jinja2-tutorial-part-1-introduction-and-variable-substitution/) και δουλεύει πλέον στην NTC, ενώ ο δεύτερος (Cisco Devnet Create 2021 Award Winner) έχει γράψει το βιβλίο Automate Your Network το οποίο πραγματεύεται τη χρήση της Ansible για Network Automation (https://www.automateyournetwork.ca/). (Ο John έκτοτε έχει γίνει Python convert και διαπρέπει μεταξύ άλλων στην ανάπτυξη με python του Merlin Open Source Project, στο οποίο έχει κάνει εκτενώς χρήση της Jinja2).

Υπάρχουν επίσης εξαιρετικά online courses που αφιερώνουν μέρος τους στην χρήση της Jinja2 με όλους τους τρόπους που αναφέραμε ήδη, αλλά και το ίδιο το documentation της Jinja. Μπορείτε να εμβαθύνετε με όποιο τρόπο προτιμάτε ή να ξεκινήσετε από την λίστα των συνδέσμων που παραθέτουμε παρακάτω.

Καλή επιτυχία!

Παραπομπές