Extending Ansible action plugins for Cisco IOS

  • by Patrick Ogenstad
  • October 30, 2017

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?

What was trying to be done?

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.

Action plugins in Ansible

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.

IOS Action Plugin

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.

Using your own action plugins

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

Extending the action plugin for ios_facts

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.

Ansible unable to open shell

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!

Ansible ImportError

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

Ansible AttributeError

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! Progress The ios_config tasks are still failing, but that’s because we haven’t created an action plugin for that module yet.

What about the username and password?

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.

Final action modules

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! Successful playbook

Conclusion

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.