Object Oriented Programming (OOP) for Network Automation

To be honest, I never thought I needed Object-Oriented Programming (OOP) for my job as a Network Engineer. I used to think it was only for software developers. My work was mostly about Network Automation, and I used functions to do that. Objects and classes seemed too complicated and not needed. But I was wrong.

In this blog post, I will share a recent experience where I had to use OOP for the first time. I will show you how I used the Palo Alto REST API with Objects and Classes. I hope this post will help you see that OOP is not just for developers. As Network Engineers, we can also use it to make our work easier. So, let's get started and dive into the world of OOP for Network Automation.

Palo Alto REST API - Quick Overview

The Palo Alto REST API is a powerful interface that enables Network Engineers to programmatically manage and automate Palo Alto Networks devices. It operates over HTTPS and allows for the manipulation of the device's configuration and retrieval of operational data using standard HTTP methods.

This RESTful API is designed to make communication with Palo Alto devices more efficient, by sending and receiving JSON payloads for various operations. From configuring security policies to gathering system logs, the API simplifies network automation, making it a highly valuable tool in a network engineer's toolkit.

If you are new to Palo Alto REST API and Python, please check out my other blog post here.

Palo Alto REST API with Python
Our goal here is to identify rules that have ‘any’ ‘any’ for both Source and Destination Addresses. As you can see below, there is only a single

As we dive into this discussion, it's crucial to illustrate a basic script that doesn't utilize functions or objects. To get a grip on this, let's consider a Python script that uses the 'requests' library to create security policies on the Panorama.

import requests
import json

# Disable self-signed warning
requests.packages.urllib3.disable_warnings()


# location = {'location': 'device-group', 'device-group': 'lab', 'name': 'google_dns'}
location = {'location': 'device-group', 'device-group': 'offices', 'name': 'google_dns'}
headers = {'X-PAN-KEY': 'YOUR_API_KEY'}
# api_url = "https://Firewall_IP/restapi/v10.2/Objects/Addresses"
api_url = "https://Firewall_IP/restapi/v10.2/Objects/AddressGroups"

body = json.dumps(
    {
        "entry":
        {
            "@name": "google_dns",
            "ip-netmask": "8.8.8.8",
        }
    }
)

r = requests.post(api_url, params=location, verify=False, headers=headers, data=body)
print(r.text)

On the surface, this script looks simple and it gets the job done. However, it's far from perfect. When we need to target a different panorama, different device-group, or even different endpoints such as addresses or address-groups, this code becomes a headache. Why? Well, we would find ourselves commenting out the old parameters and creating new ones - a process that is not only tedious but also renders the code messy and less scalable. Therefore, we need a more refined and flexible approach, and that's where the power of Object-Oriented Programming comes into play.

The OOP Approach

Now, let's look at how we can leverage the principles of Object-Oriented Programming to tidy up our code and make it more scalable. We create a separate Python file, paloaltoapi.py, to define a class called PaloAltoAPI. This class is a blueprint that encapsulates all the necessary operations for our network automation task, including methods for getting an API key, building URLs, and getting, creating, editing, and deleting resources.

If you are not familiar with Python OOP, please check out my other blog post below which will give you a basic understanding of Objects and Classes.  

Python Objected Oriented Programming (OOP) - with examples
As you start your journey in Python programming, you might wonder, “If we already have functions in Python for bundling and reusing code, why do we even need Objects and Classes?”

Define Class and Methods

# file name paloaltoapi.py
import requests
import json
import xml.etree.ElementTree as ET

class PaloAltoAPI:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        requests.packages.urllib3.disable_warnings()

    def get_key(self):
        query = {'type':'keygen', 'user':self.username, 'password':self.password}
        response = requests.get(f"{self.base_url}/api", params=query, verify=False)

        root = ET.fromstring(response.text)
        api_key = root.find(".//key").text
        return {'X-PAN-KEY': api_key}

    def build_url(self, endpoint, device_group, name=None):
        location = {'location': 'device-group', 'device-group': device_group}
        if device_group == 'shared':
            location = {'location': 'shared'}
        if name is not None:
            location['name'] = name
        return f"{self.base_url}/restapi/v10.2/{endpoint}", location

    def get_resource(self, endpoint, device_group):
        url, location = self.build_url(endpoint, device_group)
        headers = self.get_key()
        response = requests.get(url, params=location, verify=False, headers=headers)
        return response.json()['result']['entry']
    
    def create_resource(self, endpoint, device_group, payload, name):
        url, location = self.build_url(endpoint, device_group, name)
        headers = self.get_key()
        response = requests.post(url, params=location, verify=False, headers=headers, data=payload)
        return response.text
    
    def edit_resource(self, endpoint, device_group, payload, name):
        url, location = self.build_url(endpoint, device_group, name)
        headers = self.get_key()
        response = requests.put(url, params=location, verify=False, headers=headers, data=payload)
        return response.text

    def delete_resource(self, endpoint, device_group, name):
        url, location = self.build_url(endpoint, device_group, name)
        headers = self.get_key()
        response = requests.delete(url, params=location, verify=False, headers=headers)
        return response.text

Above, a class named PaloAltoAPI is defined in the file paloaltoapi.py. The class includes several methods that are designed to interact with the Palo Alto API. Upon initialization, the class takes the base URL, username, and password for accessing the API, and disables warnings for self-signed certificates. There are methods to generate the necessary API key (get_key), build the required URL for specific operations (build_url), and manipulate (get, create, edit, and delete) resources (get_resource, create_resource, edit_resource, delete_resource).

The build_url method is designed to create a URL for a specific operation. It takes three parameters: endpoint, device_group, and name (which is optional). First, it sets up the location as a dictionary that will be added to the request as parameters. This dictionary contains the key 'location' set to 'device-group' and the 'device-group' set to whatever device group is passed to the method.

Finally, the method returns a tuple with two elements. The first element is a string that represents the URL for the API request. It combines the base URL (stored in the self.base_url attribute), a fixed part of the path (/restapi/v10.2/), and the endpoint provided as an argument. The second element is the location dictionary, which contains the parameters for the request.

These methods use the Python requests library to send HTTP requests to the Palo Alto API and parse the responses, making it more straightforward to work with the API. The class encapsulates all these operations into reusable methods, providing a more organized and efficient approach to performing network automation tasks.

Some Examples

Now that I've defined the Classes and Methods, the next time when I want to make an API call, all I need is a couple of lines.

# file name main.py
from paloaltoapi import PaloAltoAPI
import json

pan_object = PaloAltoAPI('https://panorama.local', 'username', 'password')
addresses = pan_object.get_resource("Objects/Addresses", 'shared')
address_groups = pan_object.get_resource("Objects/AddressGroups", 'shared')
security_policy = pan_object.get_resource("Policies/SecurityPreRules", 'offices')

data = json.dumps(
    {
        "entry": 'dummy_data'
    }
)
edit_group = pan_object.edit_resource("Objects/AddressGroups", 'shared', data, 'group_name')

delete_rules = pan_object.delete_resource(f"Policies/SecurityPreRules", 'offices', 'rule_name')

The abovemain.py script illustrates how the PaloAltoAPI class is used in practice. An instance of the class, pan_object, is created with specific parameters - the URL of the Panorama, and the username and password for API access.

This object is then used to retrieve a variety of resources including addresses, address groups, and security policies. The script also showcases the edit_resource method, which takes in an endpoint (the resource to edit such as 'address'), a device group, a JSON-formatted data payload, and a name, and uses these to modify a specific resource on the Panorama.

The delete_resource method is similarly demonstrated, deleting a specified rule from the security pre-rules of an 'offices' device group.

Code Breakdown

import requests
import json
import xml.etree.ElementTree as ET

The script starts by importing the necessary libraries: requests for handling HTTP requests, json for processing JSON data, and xml.etree.ElementTree (aliased as ET) for parsing XML data.

class PaloAltoAPI:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        requests.packages.urllib3.disable_warnings()

Next, a class named PaloAltoAPI is defined. The __init__ function is a special method that gets called when a new object is created. This function takes the base URL, username, and password for accessing the Palo Alto API and assigns them to the object's attributes. It also disables any warnings about self-signed SSL certificates.

def get_key(self):
    query = {'type':'keygen', 'user':self.username, 'password':self.password}
    response = requests.get(f"{self.base_url}/api", params=query, verify=False)

    root = ET.fromstring(response.text)
    api_key = root.find(".//key").text
    return {'X-PAN-KEY': api_key}

The get_key method generates an API key by sending a GET request to the base URL with the parameters necessary to generate the key. The method then parses the XML response to extract the key, which it returns as a dictionary.

def build_url(self, endpoint, device_group, name=None):
    location = {'location': 'device-group', 'device-group': device_group}
    if device_group == 'shared':
        location = {'location': 'shared'}
    if name is not None:
        location['name'] = name
    return f"{self.base_url}/restapi/v10.2/{endpoint}", location

The build_url method constructs a URL for a specific API operation. The location dictionary sets the location for the operation (either a device-group or shared), and the function also checks if a specific name is provided.

Next, it checks if the device group is 'shared'. If it is, it changes the 'location' key to 'shared' in the location dictionary. This is done because shared objects are not tied to a specific device-group. Then, it checks if a name was provided. If a name is given, it adds the 'name' key to the location dictionary with its value set to the given name. Please note that 'name' is required if you want to perform post, put or delete operation.

The method then combines the base URL with the specific endpoint and returns the full URL, along with the location dictionary as parameters for the request.

def get_resource(self, endpoint, device_group):
    url, location = self.build_url(endpoint, device_group)
    headers = self.get_key()
    response = requests.get(url, params=location, verify=False, headers=headers)
    return response.json()['result']['entry']

The get_resource method is used to fetch a specific resource. The method constructs the URL and the headers (which includes the API key), sends a GET request to the API, and returns the requested resource as a JSON object.

def create_resource(self, endpoint, device_group, payload, name):
    url, location = self.build_url(endpoint, device_group, name)
    headers = self.get_key()
    response = requests.post(url, params=location, verify=False, headers=headers, data=payload)
    return response.text

The create_resource method is used to create a new resource. The method constructs the URL and the headers, sends a POST request with the provided payload to the API, and returns the response text.

def edit_resource(self, endpoint, device_group, payload, name):
    url, location = self.build_url(endpoint, device_group, name)
    headers = self.get_key()
    response = requests.put(url, params=location, verify=False, headers=headers, data=payload)
    return response.text

The edit_resource method is similar to create_resource, but it sends a PUT request to edit an existing resource and returns the response text.

def delete_resource(self, endpoint, device_group, name):
    url, location = self.build_url(endpoint, device_group, name)
    headers = self.get_key()
    response = requests.delete(url, params=location, verify=False, headers=headers)
    return response.text

Finally, the delete_resource method is used to delete a resource. It constructs the URL and headers, sends a DELETE request to the API, and returns the response text.

The PaloAltoAPI class defined in this script encapsulates all the operations required for interacting with the Palo Alto API, resulting in a more organized and reusable solution for network automation tasks.

Conclusion

In the end, using Object-Oriented Programming (OOP) for network automation can make our work a lot easier. It helps keep our code tidy and makes it simpler to change things when we need to. You might ask, "Can't we do the same with functions?" Yes, we can. But classes and objects give us more flexibility. They let us group related things together in a way that makes sense. This way, we can work with different parts of our network without having to rewrite a lot of our code.