[python]Grow commands from a template with jinja2

Problem

I made a template to push object network configuration to Cisco ASA, this is how the template looks like:

object network {{ conf_attr.object_id|e }}
{%  if conf_attr.network_type|e == "host" %}
 host {{ conf_attr.network_object1|e }}
{%  elif conf_attr.network_type|e == "range" %}
 range {{ conf_attr.network_object1|e }} {{ conf_attr.network_object2|e }}
{%  elif conf_attr.network_type|e == "subnet" %}
 subnet {{ conf_attr.network_object1|e }} {{ conf_attr.network_object2|e }}
{% endif %}
{% if "description" in conf_attr %}
 description {{ conf_attr.description|e }}
{% endif %}

conf_attr is the keyword to store the payload I sent to Cisco ASA via Nornir/netmiko, the problem with this template is only one command is sent per session.

In order to commands to be sent concurrently I will need to use threading, but I realized the commands always failed after the 5th was sent, checking from the nornir log I found that the failure was caused by connection timeout from the cisco asa.

After researching through the web, I realized Cisco ASA has a fixed maximum of 5 concurrent sessions, you cannot increase this maximum session with this command quota management-session.

So I need to re-design the jinja2 template to do two things:

  1. If conf_attr is not dictionary, then iterate the block.
  2. If it is mapping, then send the value with the key.

The trick is to consolidate the commands with jinja2 template and send this set within a single session.

Re-design the jinja2 template

This is the current template which works with multiple network object requests and single network object request. A single network object request is pushed to the API I wrote in dictionary which is matched by the jinja2’s mapping, if multiple network object requests it is pushed to the API with a list of dictionaries.

With the re-design in of jinja2 template, I only need one session to push all commands based on request dynamically.

{% if conf_attr is not mapping %}
{% for per_attr in conf_attr %}
object network {{ per_attr.object_id|e }}
{%  if per_attr.network_type|e == "host" %}
 host {{ per_attr.network_object1|e }}
{%  elif per_attr.network_type|e == "range" %}
 range {{ per_attr.network_object1|e }} {{ per_attr.network_object2|e }}
{%  elif per_attr.network_type|e == "subnet" %}
 subnet {{ per_attr.network_object1|e }} {{ per_attr.network_object2|e }}
{% endif %}
{% if "description" in per_attr %}
 description {{ per_attr.description|e }}
{% endif %}
{% endfor %}
{% else %}
object network {{ conf_attr.object_id|e }}
{%  if conf_attr.network_type|e == "host" %}
 host {{ conf_attr.network_object1|e }}
{%  elif conf_attr.network_type|e == "range" %}
 range {{ conf_attr.network_object1|e }} {{ conf_attr.network_object2|e }}
{%  elif conf_attr.network_type|e == "subnet" %}
 subnet {{ conf_attr.network_object1|e }} {{ conf_attr.network_object2|e }}
{% endif %}
{% if "description" in conf_attr %}
 description {{ conf_attr.description|e }}
{% endif %}
{% endif %}

As I only need the value from the keys I referenced in the dictionary, to do this you need to use |e, take a portion of the above template for illustration:

{% for per_attr in conf_attr %}
{{ per_attr.object_id|e }}
{% endfor %}
This is the same as 

for per_attr in conf_attr:
    per_attr["object_id"]

where conf_attr is the list, and per_attr is a dictionary from the list.

In jinja2 template you cannot use the type function to check the data type, but there are objects in jinja2 to check the types, mapping is a dict, and iterable is an object that can be iterate, this can be a list or tuple or a string. Yes a string is actually a list of characters.

To check if a key exists in the dictionary use:
{% if "description" in conf_attr %}, this is the same as if "description" in conf_attr, where conf_attr is a dictionary.
Description is an option in object network command in Cisco ASA, hence it can be excluded if not configured.

Demonstration

To properly demonstrate the modified template, I will clear config object network to remove all object networks from cisco asa.

Single request

nor42

nor43

Multiple request

As the requests is very long only a portion is shown.
nor44

nor45

Some python codes that used for testing

Below are some portion of the code, the code is not completed there are things which are not available such as codes for authentication and authorization (flask_jwt), and also there is no json data validation using reqparse.

def asa_add_config(task, template=None, payload=None, **kwargs):
    cmd = task.run(task=template_file,
                   name="Generating template",
                   template=template,
                   path=TEMPLATE_PATH,
                   conf_attr=payload)

    task.host["config"] = cmd.result

    task.run(task=netmiko_send_config,
             name="Sending configuration",
             config_commands=task.host["config"])
    task.run(task=netmiko_save_config,
             name="Saving configuration",
             cmd="write memory")

def send_net_objs_to_asa_host(hostname=None, payload=None):
    asa_host = connect_asa_host(hostname)

    response = asa_host.run(task=asa_add_config,
                            template="asa_obj_net_confg.j2",
                            payload=payload)
    return {
        "status": "success" if not response[hostname][0].failed else "failed",
        "result": response[hostname][1].result.rsplit("\n")
    }

The API gateway code, only a portion:

from flask_restful import Resource, Api
from flask import Flask, request
import yaml
from pyvault2.vault.hvault2 import create_update_kv2_secrets
from network.ciscoasa import (send_acl_to_asa_host,
                              get_acl_from_asa_host,
                              sh_run_object_network,
                              send_net_objs_to_asa_host)
from pathlib import Path
from os.path import join

host_file_path = join(Path(__file__).parent.absolute(), "inventory", "hosts.yaml")

# Create flask object
app = Flask(__name__)
# Create api object, part of Flask_RESTful, use add_resource method to add endpoint.
api = Api(app)

class AsaObject(Resource):
    """
    To read/add object network or object service.
    """
    def get(self, asa_host, object_id=None):
        response = sh_run_object_network(hostname=asa_host, object_id=object_id)
        return response if response is not None else {"message": "not found"}, \
               200 if response is not None else 404

    def post(self, asa_host):
        json_data = request.get_json()

        return send_net_objs_to_asa_host(hostname=asa_host, payload=json_data), 201
api.add_resource(AsaObject,
                 "/api/asa/objects/&ltstring:asa_host&gt",
                 "/api/asa/objects/network/&ltstring:asa_host&gt",
                 "/api/asa/objects/network/&ltstring:asa_host&gt/&ltstring:object_id&gt",
                 "/api/asa/objects/service/&ltstring:asa_host&gt",
                 "/api/asa/objects/service/&ltstring:asa_host&gt/&ltstring:object_id&gt")

There is some problem displaying the endpoints here are them:
nor46

This is the main code:

from api.app import app


if __name__ == "__main__":
    app.run(ssl_context="adhoc")

Yes you can enable ssl when running flask, the prerequisite is to install pyopenssl.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s