Isolating the Network Role: How to mock the Ansible runtime

Summary

In this post I will explain a bit about my most relevant work during July at GSoC: Running an Ansible module directly from Python and how and why it can be useful to mock Python modules sometimes.

Introduction

My main task in the GSoC project is to add Pytest to the Network Role at Linux System Roles. This will lead to a better coverage of tests, since all the utilities of pytest will be available for testing the module, instead of being limited to the execution from a playbook. This does not mean that playbooks will not be used anymore (it is still useful to see how Ansible reacts to the different tasks!), but that a more in-depth testing will be possible. It will be possible to test classes and functions individually and to use pytest features like fixture or parametrize.

In order to achieve this, I had to mock some objects. This is necessary because the module is meant to be executed only from Ansible, and thus some dependencies go missing. Furthermore, the available system paths also change depending on where you execute the module from, so it was also necessary to mock this attribute.

Why is it really necessary to make this mocking?

We need to access the different functions and classes in library/network_connections.py in order to test the module. But if we just write import library.network_connections.py a module not found error will rise for ansible.module_utils.network_lsr. This happens because in the git repository, argument_validator.py can be found inside network/module_utils/network_lsr/. On the target system, which is where we will execute the module if we run Ansible, it is located in ansible/module_utils/network_lsr/ instead. Therefore, given that we do not want to change the imports of the module, the best option is to make the system “believe” that our module_utils/ is inside of an ansible module by mocking it.

Summing it up: When we execute the module through Ansible, Ansible exports the necessary Python files to the target system (which would be: folders modules/ and module_utils/). Because we are executing the module without Ansible and in our “host” system, these ansible modules need to be mocked.

To exactly see what Ansible deploys on your target system, you can follow these steps:

  1. Execute any playbook with the flag “ANSIBLE_KEEP_REMOTE_FILES=1” to avoid the files being erased after their use.
  2. Inside the target system, search for the file which contains the compressed Ansible folder. I used find -name *network* to find the file (AnsiballZ_network_connections.py)
  3. Execute /usr/libexec/platform-python path/to/AnsiballZ_network_connections.py explode and it will expand the module into a folder.
  4. Enter the folder and then folder/ansible/module_utils
  5. Now we can see all the necessary dependencies - like module_utils/ - in this folder.

The code

{% highlight python %} with mock.patch.object(
sys,
“path”,
[parentdir, os.path.join(parentdir, “module_utils/network_lsr”)] + sys.path, ):
with mock.patch.dict(
“sys.modules”,
{“ansible”: mock.Mock(), “ansible.module_utils”: import(“module_utils”)}, ):
import library.network_connections as nc {% endhighlight %}

Changing sys.path

Thanks to the mock object library, patching the value of an attribute from the object we need (in this case, sys.path) is not a difficult task, using patch.object. This function can take up to three arguments:

  1. The object to be patched, (sys)
  2. the attribute that we want to change, (path)
  3. and the object used to replace the current value of the attribute.

The object to that will be used is a list consisting of the current sys.path but adding the main directory of the project and the module_utils/network_lsr folder. This last folder is the one that is usually imported through Ansible instead of locally, but since we need to execute the project directly from Python, we need a workaround.

Using mock modules

Similarly to the previous description, now that we have network_lsr in the path we want to import it in a way that the module thinks it’s from the Ansible library. This time, we use mock.patch.dict to patch sys.modules. Ansible gets imported as a Mock object and ansible.module_utils as module_utils.

(Note that we can import module_utils thanks to the path workaround we did before.)

Importing the library

Thanks to the previous mocking, everything is ready to import the Network Role. Once it is imported, we can already create a custom RunEnvironment for the testing and therefore running tests without the need of executing them from Ansible.

Conclusion

Although this was a nice step forward, now I’m trying to make an ansible playbook that makes the test compatible with different distributions, which is a new challenge for me. I hope I can bring more news and details on how to achieve that after August! =)

Back To Top