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:
- If conf_attr is not dictionary, then iterate the block.
- 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
Multiple request
As the requests is very long only a portion is shown.
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/<string:asa_host>", "/api/asa/objects/network/<string:asa_host>", "/api/asa/objects/network/<string:asa_host>/<string:object_id>", "/api/asa/objects/service/<string:asa_host>", "/api/asa/objects/service/<string:asa_host>/<string:object_id>")
There is some problem displaying the endpoints here are them:
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
.