[python]Capture return values after threads are finished.

Return value lost after threads finished

I have made two functions:

from nornir.plugins.tasks.networking import netmiko_send_config, netmiko_save_config

def asa_add_config(task, template=None, **kwargs):
    cmd = task.run(task=template_file,
                   name="Generating template",
                   template=template,
                   path="../network/templates/",
                   conf_attr=kwargs)
    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)
    return {
            "status": "success" if not response[hostname][0].failed else "failed",
            "result": response[hostname][1].result.rsplit("\n")
    }

The jinja template for preparing the object network {name} command:

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

Then I create a test code to push multiple object network {name} commands to a single cisco asa – fw02.

To achieve concurrency I use the threading.Thread module, however my return value of send_net_objs_to_asa_host is lost, I cannot get the return value from thread.join as the documentation has already stated that join method always returns None.

This is the code for testing:

from network.ciscoasa import send_net_objs_to_asa_host
from threading import Thread
from time import sleep


payloads = [
    {
        "object_id": "test_object5",
        "network_type": "range",
        "network_object1": "172.16.10.0",
        "network_object2": "172.16.10.10",
        "description": "set 1"
    },
    {
        "object_id": "test_host1",
        "network_type": "host",
        "network_object1": "192.168.100.100",
        "description": "Set 2"
    },
    {
        "object_id": "test_range1",
        "network_type": "range",
        "network_object1": "192.168.10.1",
        "network_object2": "192.168.10.100"
    },
    {
        "object_id": "test_net1",
        "network_type": "subnet",
        "network_object1": "10.0.0.0",
        "network_object2": "255.0.0.0",
        "description": "classful A"
    }
]

threads = []
for payload in payloads:
    t = Thread(target=send_net_objs_to_asa_host, args=("fw02", payload,))
    threads.append(t)
    t.start()
    sleep(1)

for thread in threads:
    thread.join()

commands were pushed but I got no way to get the return value.

Creating a subclass of threading.Thread and modify the behaviour of run and join

I found the answer in here. The child class added return value on run and join methods of the parent class, this is known as overriding.
Here’s the code of my sub class:

from threading import Thread
"""
See reference: 
https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread-in-python/40344234#40344234
Check the answer by GuySoft.
"""


class AsaThread(Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._return = None # child class's variable, not available in parent.

    def run(self):
        """
        The original run method does not return value after self._target is run.
        This child class added a return value.
        :return: 
        """
        if self._target is not None:
            self._return = self._target(*self._args, **self._kwargs)

    def join(self, *args, **kwargs):
        """
        Join normally like the parent class, but added a return value which
        the parent class join method does not have. 
        """
        super().join(*args, **kwargs)
        return self._return

Then on my main test code:

from network.ciscoasa import send_net_objs_to_asa_host
from time import sleep
from network.threads import AsaThread
from pprint import pprint

payloads = [
    {
        "object_id": "test_object5",
        "network_type": "range",
        "network_object1": "172.16.10.0",
        "network_object2": "172.16.10.10",
        "description": "set 1"
    },
    {
        "object_id": "test_host1",
        "network_type": "host",
        "network_object1": "192.168.100.100",
        "description": "Set 2"
    },
    {
        "object_id": "test_range1",
        "network_type": "range",
        "network_object1": "192.168.10.1",
        "network_object2": "192.168.10.100"
    },
    {
        "object_id": "test_net1",
        "network_type": "subnet",
        "network_object1": "10.0.0.0",
        "network_object2": "255.0.0.0",
        "description": "classful A"
    }
]
threads = []
response_list = []
for payload in payloads:
    t = AsaThread(target=send_net_objs_to_asa_host, args=("fw02", payload,))
    threads.append(t)
    t.start()
    sleep(1)


for thread in threads:
    response_list.append(thread.join())

pprint(response_list)

The output looks like this:
nor39
The original parent Thread does not return value on run and join, the overriding of the customed class AsaThread added the return value functionality.

See the original Thread class run method:
nor40
A method if not specified defaults to return None, this is the same with function.

See the original Thread class join method:
nor41

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