It started out as a question. If you are using several networking modules in a playbook, do you really have to repeat the same credentials on every task? Just like the last few articles about Ansible this one came to life after answering questions in a chat room. The short answer is; No you don’t have to include all of the required parameters for every task, you can use an action plugin to work around that.
Great! So what’s an action plugin?
Looking at the playbook below we define a cli variables for the credentials and then use the provider
parameter on each of the ios modules we want to use.
---
- hosts: all
connection: local
gather_facts: false
vars:
cli:
username: admin
password: Password1
tasks:
- name: Facts
ios_facts:
provider: '{{ cli }}'
- name: Baseline
ios_config:
provider: '{{ cli }}'
lines:
- 'no ip http server'
- 'no ip http secure-server'
- name: VTY
ios_config:
provider: '{{ cli }}'
parents: 'line vty 5 15'
lines: 'transport preferred ssh'
The question was:
“If I have fifty tasks in my playbook do I really have to specify the same provider for each and every task?”
While it’s only one extra line it’s hard to argue against the fact that it’s a bit redundant. Especially since we’re using a persistent connection in the background. One way to solve this could be to set the environment variables ANSIBLE_NET_USERNAME and ANSIBLE_NET_PASSWORD, at the moment this would trigger a deprecation warning since those variables are tied to the old username
and password
parameters which are being phased out. That could perhaps be considered a bug since the variables should get assigned to provider.username and provider.password instead. While using environment variables would work you might be using Ansible Vault or some other secrets store and don’t want to export the secrets as environment variables, or have to care about how they are cleared after the playbook completes. Another way to solve this issue is to look at action plugins.
Perhaps the most relevant question, if you haven’t heard about action plugins, is; Do I have to care about them? For a lot of people the answer is no. Action plugins will mostly be of interest to developers writing their own Ansible modules. However they might also be relevant for people who just want to understand how things work. Before digging into action plugins it can be helpful to start with regular Ansible modules and a good beginning would be to compare the code of the slack module to that of the template module. They both start in the same way with the variables DOCUMENTATION
and EXAMPLES
which are in fact used to generate the documentation for Ansible. Further down in the module code you see that the slack module is quite easy to understand, if you know some basic Python that is. The template module can be a bit harder to grasp. Below the documentation there’s no code what so ever. What’s up with that? Magic? If you stop to think about it the Slack module doesn’t really need any additional information, or everything it needs to work you send in as parameters from the playbook. The template module on the other hand needs access to all of the variables within the current Ansible run.
What happens is that Ansible searches for an action plugin with the same name as the module. With the case of the template module all the logic is placed in its action plugin.
Looking in the source code for all of the action plugins we can see that the networking modules generally have two action plugins. For Cisco IOS there’s ios.py and ios_config.py, the ios_config module uses the ios_config.py file. The other ios networking modules such as ios_fact, ios_command, ios_logging just use the prefix of ios and then load the ios.py action plugin. The reason that ios_config needs a separate one has to do with things like templates and creation of backups of the configuration.
Each action plugin uses the ActionModule class and triggers the run()
function when a module is called. In the source code we can see that the ios_config plugin inherits from the ios action plugin. Looking at the ios.py code see that it sets the username and password based on the values of the keys username and password within the provider parameter, or if those aren’t set it looks in the self._play_context.connection_user
and self._play_context.password
variables. If we could overwrite that part we’d be good to go.
When creating your own action plugins Ansible needs to be aware of the fact that they exist. To do this you can either place them in a directory called action_plugins placed at the base of your playbook. The other option would be to point to the directory which contains your action plugins from your ansible.cfg file.
[defaults]
action_plugins = /opt/ansible/plugins/action
First we update the playbook so it reflects to what we want to have:
---
- hosts: all
connection: local
gather_facts: false
vars:
cli:
username: admin
password: Password1
tasks:
- name: Facts
ios_facts:
- name: Baseline
ios_config:
lines:
- 'no ip http server'
- 'no ip http secure-server'
- name: VTY
ios_config:
parents: 'line vty 5 15'
lines: 'transport preferred ssh'
Running the playbook now will return the good old “unable to open shell” error. Nice.
The first goal is just to avoid typing the username and password so we’ll hardcode it within the action plugin.
/opt/ansible/plugins/action/ios.py:
from ansible.plugins.action.ios import ActionModule as _ActionModule
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
self._play_context.connection_user = 'admin'
self._play_context.password = 'Password1'
result = super(ActionModule, self).run(tmp, task_vars)
return result
The idea is that the above would set our to variables and then just call the run function from the object we inherited from. Let’s test this!
It would have been nice if that worked. When I saw this I was struggling to find out what was going on. Some import error, the ActionModule doesn’t exist in the ansible.plugins.action.ios namespace? Looking in the code I can see that it clearly does. I tried the import in another way.
import ansible.plugins.action.ios
class ActionModule(ansible.plugins.action.ios.ActionModule):
def run(self, tmp=None, task_vars=None):
self._play_context.connection_user = 'admin'
self._play_context.password = 'Password1'
result = super(ActionModule, self).run(tmp, task_vars)
return result
AttributeError on the module object? What’s going on? :) Testing with another file when instead importing ansible.module_utils.ios I validated that it wasn’t some weird naming issue where it wasn’t possible to load any other file named ios.py. There seemed to be some override happening for action plugins. Ansible used to do this for modules in the past where the from ansible.module_utils.basic import *
which was required in all modules wasn’t actually interpreted as Python code but was instead a placeholder. I didn’t go searching through the code to see if the same thing was happening here, instead I modified my code and tried to import the original action plugin in using the Python imp library instead.
import imp
ansible_path = imp.find_module('ansible')[1]
plugin_file = 'plugins/action/ios.py'
src = '{0}/{1}'.format(ansible_path, plugin_file)
ios = imp.load_source('ios', src)
class ActionModule(ios.ActionModule):
def run(self, tmp=None, task_vars=None):
self._play_context.connection_user = 'admin'
self._play_context.password = 'Password1'
result = super(ActionModule, self).run(tmp, task_vars)
return result
This time it works better! The ios_config tasks are still failing, but that’s because we haven’t created an action plugin for that module yet.
A big glaring problem here though is that I hardcoded the username and password within the action plugin. How can we access the variables we defined in the playbook? Or use something from Ansible vault? If you look at the arguments to the run()
function you might notice the task_vars
argument which actually contains everything we need.
ios.py:
import imp
ansible_path = imp.find_module('ansible')[1]
plugin_file = 'plugins/action/ios.py'
src = '{0}/{1}'.format(ansible_path, plugin_file)
ios = imp.load_source('ios', src)
class ActionModule(ios.ActionModule):
def run(self, tmp=None, task_vars=None):
if task_vars.get('cli'):
if task_vars['cli'].get('username'):
username = task_vars['cli']['username']
self._play_context.connection_user = username
if task_vars['cli'].get('password'):
self._play_context.password = task_vars['cli']['password']
result = super(ActionModule, self).run(tmp, task_vars)
return result
ios_config.py:
import imp
ansible_path = imp.find_module('ansible')[1]
plugin_file = 'plugins/action/ios_config.py'
src = '{0}/{1}'.format(ansible_path, plugin_file)
iosconfig = imp.load_source('ios_config', src)
class ActionModule(iosconfig.ActionModule):
def run(self, tmp=None, task_vars=None):
if task_vars.get('cli'):
if task_vars['cli'].get('username'):
username = task_vars['cli']['username']
self._play_context.connection_user = username
if task_vars['cli'].get('password'):
self._play_context.password = task_vars['cli']['password']
result = super(ActionModule, self).run(tmp, task_vars)
return result
No everything works. Mission accomplished!
For my part i don’t know if I use that many tasks in each playbook so that adding the provider argument to each one is that much of a hassle. Still, I like the fact that I can work around it if I would want to. This article also shows you how you can work with action plugins and create your one ones. Perhaps you like Ansible but aren’t too fond of Jinja and want to make a mako_template module. It might be that you want to create something for the Napalm or ntc-ansible modules. As long as you create your own modules and don’t want to extend existing action plugins you won’t have to worry about the import workaround I implemented above and you should be able to use import the parent ActionModule from another plugin, or ActionBase from ansible.plugins.action.
A final word of warning, the above action plugins were written for Ansible 2.4, they might or might not work for future versions of Ansible.