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:
- Execute any playbook with the flag “
ANSIBLE_KEEP_REMOTE_FILES=1
” to avoid the files being erased after their use. - 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
) - Execute
/usr/libexec/platform-python path/to/AnsiballZ_network_connections.py explode
and it will expand the module into a folder. - Enter the folder and then
folder/ansible/module_utils
- 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:
- The object to be patched, (
sys
) - the attribute that we want to change, (
path
) - 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! =)