JSON-RPC
NX-API CLI

Python Requests using NX-API CLI JSON-RPC

This section is very similar to the NX-API JSON section you just completed. The key difference is the usage of JSON-RPC and how the payload is generated. You will use JSON-RPC payloads in your requests to configure an OSPF process and OSPF Ethernet interfaces for the Underlay in your staging fabric.




Step 1 - Create a New Python File in Visual Studio Code

In your VSCode Terminal, lets make use of the code keyword to open another new file for creating a Python NX-API CLI JSON-RPC script:

    
        code -r /home/cisco/Documents/nxapilab/scripts/nxapi_cli_jsonrpc.py
    

Step 2 - Python Import Statements

In your VSCode, a new, unsaved file should now be open called nxapi_cli_jsonrpc.py.

In your new Python file, you need to import the same packages as before. Either copy or type the import statements below and place them at the top of your Python file.

    
        import argparse
        import pathlib
        import sys
        import jinja2
        import yaml
        import json
        import requests


        requests.packages.urllib3.disable_warnings()


        


Step 3 - Python Function Definition to Build NX-API CLI JSON-RPC Payloads

The function definition below, like in the previous section, is used to dynamically generate your JSON-RPC configuration payload and adheres to the NXAPI CLI JSON-RPC interface's payload structure. The function definition, nxapi_payload_builder, takes a single argument, commands, which is the raw, line for line cli commands that you know today and breaks them apart line by line into a list of strings for each command. The cli commands are generated from a Jinja template file that you will define later in this section. The list of string commands is then iterated over in a for loop and added to the JSON-RPC structure as the value for the key cmd. The for loop also uses the enumerate method to create an enumerate count object. The enumerate count object is used to create the id key value pair in the JSON-RPC payload and is used to identify the order of the commands in the payload. Each iteration of the for loop will append the JSON-RPC payload to a list object, payload, which is returned at the end of the function.

Either copy or type the code below and place it next into your Python file.

    
        def nxapi_payload_builder(commands):
            cmds = [command.strip() for command in commands.splitlines()]

            payload = []

            for id, cmd in enumerate(cmds):
                jsonrpc_template = {
                    "jsonrpc": "2.0",
                    "method": "cli",
                    "params": {
                        "cmd": cmd,
                        "version": 1
                    },
                    "id": id + 1,
                    "rollback": "rollback-on-error"
                }
                payload.append(jsonrpc_template)

            return payload


        


Step 4 - Python Function Definition to Perform the POST Operation to the NX-API Interface

After nxapi_payload_builder, this next function is identical to the nxapi_post function used in nxapi_cli_json.py. The difference in how this function is invoked comes down to the headers passed for JSON-RPC. This will be set in the next step.

Either copy or type the code below and place it next into your Python file.

    
        def nxapi_post(url, headers, payload, username, password):
            try:
                print(f"Payload sent to {url}:")
                print(payload)
                response = requests.post(url, json=payload, headers=headers, auth=(username, password), verify=False)
                print(f"Response from {url}:")
                if response.ok:
                    print(json.loads(response.text))
                else:
                    print("Error! " + str(response.status_code))
                print()
            except requests.exceptions.RequestException as e:
                raise SystemExit(e)


        


Step 5 - Python Main Function

After you have successfully created your function definitions, it's time to create the main method, that is the entry point for your Python script. It will perform the function calls you just defined based on your command line arguments. The first part of the code below will use argparse to define these command line arguments with subsequent if condition checks to ensure the proper file types are being used. If the proper file types are used, the code will then open the data file and load the contents into a Python dictionary using the yaml.safe_load() function. Additionally, the code will open the Jinja template file, which is to be defined in the next step, and load the contents into a variable called template.

The authentication credential variables for the switches are also defined by referencing the dictionary keys and values from the data file. The REST header is also defined as a dictionary with the key/value pair of {'content-type': 'application/json-rpc'} since you are working with the NXAPI CLI JSON-RPC interface.

The last section of the code, is how you iterate over the devices defined in your data file that make up your staging fabric. This is done by again referencing the dictionary keys and values from the data file down to the devices key. Then using the .keys() dictionary method to iterate over the dictionary keys, which are the switch names.

Using the data for each switch device, the code will then render the Jinja template using the template.render() function. However, this time, the usage or template.render() differs slightly; two variables, defaults and device are passed to the template as dictionary objects using the keys from your data file. What this means is that the defaults variable is a dictionary that has your data file defaults and for each iteration in the for loop, the device variable is a dictionary that has the data file device specific data. This renders the raw CLI that you know today using the Jinja template that is to be defined in the next step.

The NXAPI CLI JSON-RPC payload is then generated using the nxapi_payload_builder function you defined earlier; again this function will take the raw CLI and generate the semi-colon seperated string of commands that is used as the input in the payload. Lastly, the switch(s) REST interface is called using the nxapi_post function you also defined earlier, where this function takes the URL, header, payload, and authentication credentials as arguments to configure the switch via the NXAPI CLI JSON-RPC interface.

Either copy or type the code below and place it next into your Python file.

    
        def main():
            parser = argparse.ArgumentParser()

            parser.add_argument("-d", "--data", type=pathlib.Path)
            parser.add_argument("-t", "--template", type=pathlib.Path)
            parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")

            args = parser.parse_args()

            if args.data.suffix in (".yaml", ".yml"):
                with open(args.data) as f:
                    data = yaml.safe_load(f)
            else:
                sys.exit(f"{args.data.suffix} is not a supported file format.")

            if args.template.suffix != ".j2":
                sys.exit(f"{args.template.suffix} is not a supported template format.")

            with open(args.template) as f:
                template = jinja2.Template(f.read())

            username = data['fabric']['defaults']['credentials']['username']
            password = data['fabric']['defaults']['credentials']['password']
            headers = {'content-type': 'application/json-rpc'}

            for device in data['fabric']['devices'].keys():
                commands = template.render(defaults=data['fabric']['defaults'], device=data['fabric']['devices'][device])
                payload = nxapi_payload_builder(commands)
                url = f"https://{data['fabric']['devices'][device]['mgmt']}/ins"
                nxapi_post(url, headers, payload, username, password)


        if __name__ == '__main__':
            main()
        


Step 6 - OSPF Jinja Template

With your Python script defined, it's time to create the Jinja template file that will be used to generate the OSPF configuration for each switch in your fabric. Once again, the Jinja template will be driven by the data file you defined in the first section.

However, this time, the key takeaway from this Jinja template is how to combine using the template with fabric-wide or network-wide defaults vs device specific data. Jinja offers a default keyword that can be used to define a default value for a variable given one does not exist, such as in the case of a device specific variable.

Much like the previous interface Jinja template, if conditions are used to determine what configuration to generate based on the data file. To start, if the defaults['ospf'] or device['ospf'] (device specific OSPF data) key exists in the data file, then the OSPF process and area will be configured using the values defined in the data file, otherwise, this template will not generate any OSPF configuration.

Next, how to set a Jinja variable, device_ospf, is performed to determine if their is device specific OSPF data defined in the data file and if not, set a None value. If there is device specific OSPF data defined in the data file, then the device_ospf variable will be used to set the router OSPF process otherwise, it will fallback to using the default values as annotated by the pipe then Jinja default keyword; device_ospf['process'] | default(defaults['ospf']['process']). The router-id is derived from the specific device.

Lastly, a for loop is used to iterate over each device's interfaces and based on the port_type being defined as routed, will create the configuration with the OSPF process and area defined in the data file, either specific to the device's interface or using the data file defaults.

    
        touch /home/cisco/Documents/nxapilab/scripts/templates/ospf.j2
        cat <<EOF >> /home/cisco/Documents/nxapilab/scripts/templates/ospf.j2
        {% if defaults['ospf'] or device['ospf'] -%}
        feature ospf
        {% set device_ospf = device.get('ospf', None) -%}
        router ospf {{ device_ospf['process'] | default(defaults['ospf']['process']) }}
        router-id {{ device['router_id'] }}
        {% for interface in device['interfaces'] -%}
        interface {{ interface['name'] }}
        {% if interface['port_type'] == "routed" -%}
        ip router ospf {{ device_ospf['process'] | default(defaults['ospf']['process']) }} area {{ device_ospf['area'] | default(defaults['ospf']['area']) }}
        ip ospf network {{ device_ospf['network_type'] | default(defaults['ospf']['network_type']) }}
        {% endif -%}
        {% endfor -%}
        {% endif -%}
        EOF
        


Step 7 - Execute Python Script

Upon successfully constructing the Python code above, save your nxapi_cli_jsonrpc.py file using Ctrl+s on the Windows keyboard or by clicking File then Save.

Warning

Be sure to save your file! Not saving will result in your code not executing.

Return to the VSCode Terminal command line to execute your code using:

    
        python nxapi_cli_jsonrpc.py --data=data.yaml --template=templates/ospf.j2
    

Printed below is the code execution on the switches in your staging fabric; you can see the JSON-RPC payload sent to the switch(s) and the response from the switch(s):

Make note of the payload list and how each entry for cmd is a single command that were generated and split into a list from the Jinja template.

    Payload sent to https://10.15.1.11/ins:
    [{'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'feature ospf', 'version': 1}, 'id': 1, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router ospf Underlay', 'version': 1}, 'id': 2, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router-id 10.10.10.11', 'version': 1}, 'id': 3, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'interface Ethernet1/1', 'version': 1}, 'id': 4, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip router ospf Underlay area 0.0.0.0', 'version': 1}, 'id': 5, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip ospf network point-to-point', 'version': 1}, 'id': 6, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'interface Ethernet1/2', 'version': 1}, 'id': 7, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip router ospf Underlay area 0.0.0.0', 'version': 1}, 'id': 8, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip ospf network point-to-point', 'version': 1}, 'id': 9, 'rollback': 'rollback-on-error'}]
    Response from https://10.15.1.11/ins:
    [{'jsonrpc': '2.0', 'result': None, 'id': 1}, {'jsonrpc': '2.0', 'result': None, 'id': 2}, {'jsonrpc': '2.0', 'result': None, 'id': 3}, {'jsonrpc': '2.0', 'result': None, 'id': 4}, {'jsonrpc': '2.0', 'result': None, 'id': 5}, {'jsonrpc': '2.0', 'result': None, 'id': 6}, {'jsonrpc': '2.0', 'result': None, 'id': 7}, {'jsonrpc': '2.0', 'result': None, 'id': 8}, {'jsonrpc': '2.0', 'result': None, 'id': 9}]

    Payload sent to https://10.15.1.12/ins:
    [{'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'feature ospf', 'version': 1}, 'id': 1, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router ospf Underlay', 'version': 1}, 'id': 2, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router-id 10.10.10.21', 'version': 1}, 'id': 3, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'interface Ethernet1/10', 'version': 1}, 'id': 4, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip router ospf Underlay area 0.0.0.0', 'version': 1}, 'id': 5, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip ospf network point-to-point', 'version': 1}, 'id': 6, 'rollback': 'rollback-on-error'}]
    Response from https://10.15.1.12/ins:
    [{'jsonrpc': '2.0', 'result': None, 'id': 1}, {'jsonrpc': '2.0', 'result': None, 'id': 2}, {'jsonrpc': '2.0', 'result': None, 'id': 3}, {'jsonrpc': '2.0', 'result': None, 'id': 4}, {'jsonrpc': '2.0', 'result': None, 'id': 5}, {'jsonrpc': '2.0', 'result': None, 'id': 6}]

    Payload sent to https://10.15.1.13/ins:
    [{'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'feature ospf', 'version': 1}, 'id': 1, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router ospf Underlay', 'version': 1}, 'id': 2, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'router-id 10.10.10.22', 'version': 1}, 'id': 3, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'interface Ethernet1/10', 'version': 1}, 'id': 4, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip router ospf Underlay area 0.0.0.0', 'version': 1}, 'id': 5, 'rollback': 'rollback-on-error'}, {'jsonrpc': '2.0', 'method': 'cli', 'params': {'cmd': 'ip ospf network point-to-point', 'version': 1}, 'id': 6, 'rollback': 'rollback-on-error'}]
    Response from https://10.15.1.13/ins:
    [{'jsonrpc': '2.0', 'result': None, 'id': 1}, {'jsonrpc': '2.0', 'result': None, 'id': 2}, {'jsonrpc': '2.0', 'result': None, 'id': 3}, {'jsonrpc': '2.0', 'result': None, 'id': 4}, {'jsonrpc': '2.0', 'result': None, 'id': 5}, {'jsonrpc': '2.0', 'result': None, 'id': 6}]

Continue to next section for using NX-API REST in a similar Python script to perform PIM configuration based on your data file for switches in your staging fabric.