Services
Blog
English
Quand votre logique d’orchestration devient compliquée, vous pouvez vous
retrouver avec beaucoup de code YAML difficilement lisible, et ce n’est pas
très agréable à maintenir, par exemple quand vous mutez des variables avec des
combinaisons de filtres de templates, set_fact et autres. La solution est de
passer le code logique de YAML à Python, qui est un vrai langage de
programmation.
Comme vous le savez peut-être : les modules Ansible sont téléversés sur l’hôte cible, exécutés là-bas, puis leur résultat est renvoyé à Ansible.
Ici, nous présentons les plugins d’action Ansible. Ils s’exécutent sur l’hôte de contrôle, peuvent afficher des données en direct, accéder à toutes les variables de l’inventaire et exécuter n’importe quel nombre de modules, par exemple.
Créez un nouveau rôle avec un répertoire action_plugins :
ansible-galaxy role init example
mkdir example/action_plugins
Créez un script example/action_plugins/example.py :
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
result = dict(changed=False)
return result
Appelez le plugin depuis votre script example/tasks/main.yml :
---
- name: Example action
example:
Et un playbook :
- hosts: localhost
connection: local
roles:
- example
Lancez-le avec :
ansible-playbook test.yml
Vous ne pouvez pas utiliser la fonction Python print() dans les plugins
Ansible. À la place, vous devez utiliser l’objet Display fourni, disponible
dans l’objet action sous self._display.
L’équivalent de print() est self._display.display(), et il affichera
toujours. Mais d’autres méthodes sont disponibles, comme
self._display.verbose(), self._display.warning(), self._display.error().
La méthode verbose() permet de passer un niveau de verbosité. Si Ansible
s’exécute avec au plus ce niveau de verbosité, alors le message est affiché ;
sinon il ne l’est pas.
Par exemple :
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
result = dict(changed=False)
self._display.display('Always printed')
self._display.verbose('Verbose message', caplevel=0)
self._display.verbose('Very verbose message', caplevel=1)
return result
Cela produira différentes sorties selon la verbosité d’exécution d’Ansible.
Sans aucun flag verbose, seul le message display() est affiché :
$ ansible-playbook test.yml
PLAY [localhost] *******************************************************************************************
TASK [example : Example action] ****************************************************************************
Always printed
ok: [localhost]
PLAY RECAP *************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Avec un -v, seuls display() et verbose(..., caplevel=0) sont affichés :
$ ansible-playbook test.yml -v
PLAY [localhost] *******************************************************************************************
TASK [example : Example action] ****************************************************************************
Always printed
Verbose message
ok: [localhost]
PLAY RECAP *************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Et avec -vv, tous nos messages sont affichés :
$ ansible-playbook test.yml -vv
PLAY [localhost] *******************************************************************************************
TASK [example : Example action] ****************************************************************************
task path: /home/jpic/src/action_plugin_example/example/tasks/main.yml:2
Always printed
Verbose message
Very verbose message
ok: [localhost] => {"changed": false}
PLAY RECAP *************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Notez que caplevel doit être entre 0 et 5, et que des noms de méthodes
raccourcis sont disponibles. Le code ci-dessus aurait pu être écrit ainsi :
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
result = dict(changed=False)
self._display.display('Always printed')
self._display.v('Verbose message')
self._display.vv('Very verbose message')
return result
Notez que les méthodes d’affichage verbose acceptent un argument hôte, donc :
self._display.vv('Very verbose message', 'some-host')
S’affichera comme :
<some-host> Very verbose message
D’autres méthodes d’affichage peuvent être utiles :
self._display.warning(message) : afficher un avertissementself._display.error(message) : afficher une erreurself._display.deprecated(message) : afficher un message dans un
avertissement de dépréciation, pour conseiller à l’utilisateur de mettre
quelque chose à niveauExemple :
self._display.warning('Example warning')
self._display.error('Example error')
self._display.deprecated('Example deprecation')
Sortie :
[ERROR]: Example error
[WARNING]: Example warning
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: Example deprecation. This feature will be removed in the future.
Comme nous ne pouvons pas utiliser print() directement pour afficher des
choses, nous ne pouvons pas non plus utiliser input() pour lire. À la place,
nous devons utiliser la méthode prompt_until de l’objet Display. Exemple :
from ansible.module_utils._text import to_text
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
your_input = to_text(self._display.prompt_until(
'Type your input here:',
))
self._display.display('You typed: ' + your_input)
return dict(changed=False)
Sortie :
TASK [example : Example action] ****************************************************************************
Type your input here: My message
You typed: My message
Dans task_vars, vous obtenez toutes les variables qui seraient disponibles
en YAML, comme les facts. Exemple :
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
self._display.display(f'Running for {task_vars["inventory_hostname"]}')
self._display.display(f'BTW I use {task_vars["ansible_os_family"]}')
Affichera :
TASK [example : Example action] ********************************************************
Running for localhost
BTW I use Archlinux
Utilisez le débogueur, décrit plus loin, pour découvrir toutes les clés
disponibles dans task_vars. Mais notez que vous pouvez accéder à tout, y
compris hostvars, qui contient les variables de chaque hôte. Ainsi, vous avez
déjà la main sur le large éventail de variables que le runtime Ansible a à
offrir, ce qui est déjà beaucoup plus que ce à quoi vous êtes probablement
habitué avec les modules.
Comme les modules, les plugins d’action Ansible peuvent accepter des arguments.
Modifiez votre example/tasks/main.yml ainsi :
- name: Example action
example:
foo: bar
some:
nested: variable
alist:
- item
Les variables seront disponibles dans self._task.args. Dumppez-les ainsi :
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
self._display.display(repr(self._task.args))
return dict(changed=False)
Sortie :
TASK [example : Example action] ********************************************************
{'alist': ['item'], 'foo': 'bar', 'some': {'nested': 'variable'}}
ok: [localhost]
Avant Ansible 2.19, vous pouviez simplement utiliser madbg tel quel. Installez-le avec :
pip install madbg
Et dans votre module, ajoutez l’incantation import madbg; madbg.set_trace() :
from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
import madbg; madbg.set_trace()
return dict(changed=False)
Relancez votre playbook :
$ ansible-playbook test.yml
PLAY [localhost] ***********************************************************************
TASK [Gathering Facts] *****************************************************************
ok: [localhost]
TASK [example : Example action] ********************************************************
Waiting for debugger client on 127.0.0.1:3513
Dans un autre terminal, lancez madbg connect :
$ madbg connect
> /home/jpic/src/action_plugin_example/example/action_plugins/example.py(9)run()
7 class ActionModule(ActionBase):
8 def run(self, tmp=None, task_vars=None):
----> 9 import madbg; madbg.set_trace()
10 result = dict(changed=False)
11 self._display.display(cli2.render([*task_vars.keys()]))
ipdb>
Et vous êtes maintenant dans un débogueur IPython. Si vous ne savez pas l’utiliser, consultez ce tutoriel Python-fu sur le débogage pdb.
Cependant, quand on essaie d’utiliser madbg ou pdb avec ansible-core >= 2.19, ce qui précède produira quelque chose comme :
[ERROR]: Task failed: I/O operation on closed file
Origin: /home/jpic/src/action_plugin_example/example/tasks/main.yml:2:3
1 ---
2 - name: Example action
^ column 3
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Task failed: I/O operation on closed file"}
Et une traceback pour la même erreur dans madbg connect. Cela a été
introduit par le patch Not inherit from
stdio, qui fait que le worker
se détache de stdin.
La solution consiste à shunter la méthode de détachement. D’abord, trouvez où Ansible est installé :
$ python -c 'import ansible; print(ansible.__path__)'
['/home/jpic/.local/lib/python3.13/site-packages/ansible']
Trouvez-y la fonction _detach avec grep par exemple :
$ grep -r def._detach /home/jpic/.local/lib/python3.13/site-packages/ansible
/home/jpic/.local/lib/python3.13/site-packages/ansible/executor/process/worker.py: def _detach(self) -> None:
Et commentez la ligne où il fait sys.stdin.close(). Ensuite, madbg connect fonctionnera à nouveau.
Maintenant que nous avons couvert les bases, passons aux choses vraiment amusantes : remplacer YAML par Python pour gérer une complexité plus grande, ce qui signifie que nous allons appeler des modules comme nous le faisons en YAML, mais en Python.
Dans cet exemple, nous allons lancer les modules Ansible “file” et “copy”, équivalents à ce YAML :
- copy:
src: /tmp/src
dest: /tmp/dest
En Python :
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
# make sure we have something in /tmp/src
with open('/tmp/src', 'w') as f:
f.write('foo')
# actually execute the copy module
copy_result = self._execute_module(
'copy',
module_args=dict(
src='/tmp/src',
dest='/tmp/dest',
),
task_vars=task_vars,
)
# we could create our own result dict here too
return copy_result
Résultat :
TASK [example : example] ********************************************************************************
ok: [localhost] => changed=false
checksum: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
dest: /tmp/dest
gid: 1000
group: jpic
md5sum: acbd18db4cc2f85cedef654fccc4a4d8
mode: '0644'
owner: jpic
size: 3
src: /tmp/src
state: file
uid: 1000
Le module shell est en réalité un plugin d’action, comme on peut le voir dans le code source d’Ansible, parce qu’il se trouve dans le répertoire plugins/action.
Dans cet exemple, nous allons lancer ce YAML simple :
- shell: date
En Python :
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
# Create a new Ansible Task in Python
new_task = self._task.copy()
# So that we can set our args in Python
new_task.args = dict(_raw_params='date')
# And load the shell action
shell_action = self._shared_loader_obj.action_loader.get(
'ansible.builtin.shell',
task=new_task,
connection=self._connection,
play_context=self._play_context,
loader=self._loader,
templar=self._templar,
shared_loader_obj=self._shared_loader_obj,
)
result = shell_action.run(task_vars=task_vars.copy())
return result
TASK [example : Example action] ********************************************************
changed: [localhost] => changed=true
cmd: date
delta: '0:00:00.007122'
end: '2025-11-05 15:15:14.996482'
msg: ''
rc: 0
start: '2025-11-05 15:15:14.989360'
stderr: ''
stderr_lines: <omitted>
stdout: Wed Nov 5 15:15:14 CET 2025
stdout_lines: <omitted>
Tirez davantage des plugins d’action avec notre framework de plugins d’action Ansible