Existing code that overrides a strategy does not work in Ansible 6+

Hi,

I’m trying to override a base strategy in Ansible 6+.
So far, my code works perfectly fine on Ansible 5 and less, but as soon as I try it on a more recent version, it fails.

To illustrate my problem, I made a small reproducer.
A dumb playbook (which targets the default strategy, linear):

---
- hosts: localhost
  gather_facts: no
  tasks:
    - ansible.builtin.debug:
        msg: "Hello"

The overriding strategy (located in /usr/share/ansible/plugins/strategy/linear.py) :

#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os
import sys

from ansible.plugins.strategy.linear import StrategyModule

class StrategyModule(StrategyModule):

    def __init__(self, tqm):
        super(StrategyModule, self).__init__(tqm)

    def run(self, iterator, play_context):
        print("Customizing the linear strategy")
        return super(StrategyModule, self).run(iterator, play_context)

(the code is very similar to what we can find in the debug strategy)

Executing this code in a Ansible 5 Venv :

(ansible5) [root@max-rhel8 ~]# ansible-playbook main.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ************************************************************************************************************************************
Customizing the linear strategy

TASK [debug] ****************************************************************************************************************************************
ok: [localhost] => {
    "msg": "Hello"
}

PLAY RECAP ******************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Executing the very same code in a Ansible 6 Venv :

(ansible6) [root@max-rhel8 ~]# ansible-playbook main.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ************************************************************************************************************************************
ERROR! Unexpected Exception, this is probably a bug: cannot import name 'StrategyModule' from 'ansible.plugins.strategy.linear' (/usr/share/ansible/plugins/strategy/linear.py)
to see the full traceback, use -vvv

I read in a Mitogen issue stating that calling the ansible.plugins.strategy package is generally a bad idea :

Regardless it's probably common across versions - "ansible.plugins.strategy.*" is a magic namespace managed by Ansible's PluginLoader.
The solution is probably to avoid direct import, but asking PluginLoader to do it for us.

Source: ansible: 'module' object has no attribute 'linear' when first play doesn't use mitogen · Issue #294 · mitogen-hq/mitogen · GitHub

I then try to read about the PluginLoader and especially the strategy_loader, but I don’t think it’s the same usage and on top of that I don’t know how to use it.
I’ve found very few usecases and that kind of code doesn’t help me much.

    from ansible.plugins.loader import strategy_loader
    from ansible.plugins.strategy import StrategyBase
[ ... ]
    strategy = strategy_loader.get(new_play.strategy, self)

If anyone has some insight on that one, I would be grateful.
Thanks.

I’m not a python developer so take this with a grain of salt.

It looks to me like you’re replacing the linear strategy, not overriding/customizing it. You’re trying to import a class that doesn’t exist because you didn’t define it in your ansible.plugins.strategy.linear library.

I think the solution would be to create a new strategy by a different name that piggybacks off the linear strategy, so that your custom_linear strategy does not replace it. Then set your ansible.cfg to use your strategy by default.

Then again, I’m no python expert. I wouldn’t have thought what you’re doing would have worked in older versions of Ansible either.

Hey Caleb, thanks for your reply.

I’m not a Python develop either, but I thought I understood the Python classpath mechanism. Ansible is built upon Python and therefore uses the Python mechanisms.

What I don’t get is how changing the Ansible version could lead to a loading behavior change whereas I’m using the same Python version (3.8.6) in my virtual environments. Moreover, if I try to import the class in a Python prompt, it works just fine.

Else, your proposal is similar to the one I got from ChatGPT :slight_smile:
But I do want to “override” the default strategies, for security reasons (I want all the playbooks to get through a verification process before starting).

For now, I have two workarounds and the most elegant one is to simply make a symbolic link on the final strategy and refering to that one in my overriding strategy.

But I would love to understand what changed between Ansible 5 and 6. I diffed several files (__init__py in the strategy folder, the plugin loader, …) but I could not get a clear explanation.
I’m not stuck but a little bit confused.