Aller au contenu

Monkeyble un callback Ansible de tests unitaires

logo ansible

Je vous propose de découvrir monkeyble, une collection Ansible qui peut vous aider à tester vos playbooks mais pas que. En effet, il permet aussi de combler un manque au mode check, qu’il fonctionne dans tous les cas via un mode patching. Voyons cela en détail

Introduction

Il arrive parfois que certains playbooks ou rôles Ansible ne fonctionnent plus suite à une montée de version d’une collection, voir d’Ansible. Et souvent, il est compliqué de détecter la cause principale de ce dysfonctionnement. Monkeyble permet de mettre en place des tests unitaires sur les données en entrée et en sortie des modules.

Dans certains contextes complexes, il est aussi compliqué d’obtenir des environnements de tests à l’image de la production. De plus le mode check demande de mettre en place des ‘mocks’, des applications qui simulent le fonctionnement des applications réelles pour qu’ils fonctionnent. Et là aussi monkeyble apporte une solution plus simple et permettant de modifier les réponses d’un module à la volée permettant à un playbook lancé dans ce mode d’aller au bout sans échec.

Installation de Monkeyble

L’installation peut se faire de plusieurs façons, mais je fais le choix de l’installer classiquement via un fichier requirements.yml :

---
roles:
- name: openscap
version: "1.0.4"
scm: git
src: https://github.com/stephrobert/ansible-role-openscap.git
collections:
- name: hpe.monkeyble
version: "1.2.0"

Pour gérer finement vos dépendances par projet, je vous conseille de les installer localement en créant un fichier ansible.cfg. Profitons-en pour activer le callback :

[defaults]
nocows = True
collections_paths = ./
roles_path = ./roles
callbacks_enabled = caradoc,hpe.monkeyble.monkeyble_callback

Vous remarquez que j’installe également caradoc, [un autre callback dont je vous ai parlé il y a quelques jours] (/post/ansible-callback-caradoc/) qui permet d’écrire des traces complètes localement de toutes les taches de vos playbooks. À eux deux, à qui on ajoute molecule et on a tout ce qu’il faut pour valider et debuger votre infra-as-code Ansible dans un pipeline de CI. Je lance l’installation :

Terminal window
ansible-galaxy install -r requirements.yml --force
Starting galaxy role install process
- changing role openscap from 1.0.4 to 1.0.4
- extracting openscap to /home/vagrant/Projets/work/ansible/roles/ansible-role-hardening_os/roles/openscap
- openscap (1.0.4) was installed successfully
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/download/hpe-monkeyble-1.2.0.tar.gz to /home/vagrant/.ansible/tmp/ansible-local-18275s395vv9b/tmpxupk2wjn/hpe-monkeyble-1.2.0-6adlq47l
Installing 'hpe.monkeyble:1.2.0' to '/home/vagrant/Projets/work/ansible/roles/ansible-role-hardening_os/ansible_collections/hpe/monkeyble'
hpe.monkeyble:1.2.0 was installed successfully

Utilisation du framework de test Monkeyble

Bon tout est en place, reste plus qu’à tester son fonctionnement.

Création de tests unitaires

Tests sur les variables en entrée

Je reprends le test présent sur la documentation de Monkeyble :

Il faut créer un fichier de test monkeyble.yml : avec ce contenu :

monkeyble_scenarios:
validate_test_1:
name: "Monkeyble hello world"
tasks_to_test:
- task: "Debug task"
test_input:
- assert_equal:
arg_name: msg
expected: "Hello Monkeyble"

La structure est assez simple. On voit que l’on peut créer plusieurs jeux de test en les nommant. On retrouve aussi la syntaxe utilisée dans Ansible. Les tâches à tester se déclarent en récupérant le nom utilisé dans la tache ansible, donc attention à bien les reprendre en les copiant/collant. Pour plus de clarté voici le code du playbook test.yml :

- name: Testing play
hosts: localhost
connection: local
gather_facts: false
become: false
tasks:
- name: Debug task
ansible.builtin.debug:
msg: "Hello Monkeyble"

Monkeyble permet deux types de tests:

  • les tests en entrée de taches test_input sur les variables, un peu comme avec le module assert d’Ansible
  • les tests en sortie de taches test_output, nous verrons cela dans un autre test

Lançons notre test :

Terminal window
ansible-playbook -i localhost, -c local -e "@monkeyble.yml" -e "monkeyble_scenario=validate_test_1" test.yml
PLAY [Testing play] ***************
🐵 Starting Monkeyble callback
monkeyble_scenario: validate_test_1
Monkeyble scenario: Monkeyble hello world
TASK [Debug task] ****************
ok: [localhost] => {
"msg": "Hello Monkeyble"
}
PLAY RECAP *****************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
🐵 Monkeyble - ALL TESTS PASSED - scenario: Monkeyble hello world

Monkeyble prend en charge les méthodes de tests de la librairie unittest de Python:

  • assert_equal
  • assert_not_equal
  • assert_in
  • assert_not_in
  • assert_true
  • assert_false
  • assert_is_none
  • assert_is_not_none
  • assert_list_equal
  • assert_dict_equal

Tests sur les dictionnaires en sortie de tâches

On peut également créer des tests sur les dictionnaires retournés par la tâche. Nous utiliser un autre playbook :

- name: Hello Monkeyble
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
who: "Monkeyble"
tasks:
- name: First task
ansible.builtin.set_fact:
hello_to_who: "Hello {{ who }}"

Modifions notre test comme ceci :

monkeyble_scenarios:
validate_test_1:
name: Hello Monkeyble
tasks_to_test:
- task: First task
test_output:
- assert_equal:
result_key: result.ansible_facts.hello_to_who
expected: "Hello Monkeybl"

Si vous êtes attentif, j’ai supprimé une lettre sur le résultat attendu, donc il devrait sortir en erreur. Lançons-le :

Terminal window
ansible-playbook -i localhost, -c local -e "@monkeyble.yml" -e "monkeyble_scenario=validate_test_1" test.yml
PLAY [Hello Monkeyble] ***********
🐵 Starting Monkeyble callback
monkeyble_scenario: validate_test_1
Monkeyble scenario: Hello Monkeyble
TASK [First task] *********
ok: [localhost]
🙊 Monkeyble failed scenario ❌: Hello Monkeyble
{"task": "First task", "monkeyble_passed_test": [], "monkeyble_failed_test": [{"test_name": "assert_equal", "tested_value": "Hello Monkeyble", "expected": "Hello Monkeybl"}]}

Nickel ca fonctionne comme attendu.

Tests sur l’état d’une tâche

On peut aussi ajouter des tests d’états des tâches. Modifions notre playbook en demandant l’installation d’un package sans les droits :

- name: Hello Monkeyble
hosts: localhost
connection: local
gather_facts: false
become: false
tasks:
- name: First task
ansible.builtin.package:
name: htop
monkeyble_scenarios:
validate_test_1:
name: Hello Monkeyble
tasks_to_test:
- task: First task
should_be_skipped: true

On relance :

Terminal window
ansible-playbook -i localhost, -c local -e "@monkeyble.yml" -e "monkeyble_scenario=validate_test_1" test.yml
PLAY [Hello Monkeyble] *************
🐵 Starting Monkeyble callback
monkeyble_scenario: validate_test_1
Monkeyble scenario: Hello Monkeyble
TASK [First task] *****************
ok: [localhost]
PLAY RECAP ******
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
🐵 Monkeyble - ALL TESTS PASSED - scenario: Hello Monkeyble

La tâche a bien planté et le test est donc valide. On peut aussi tester :

  • should_be_skipped: false
  • should_be_changed: true

Mise en place de Mocks

Monkeyble permet de créer des mocks, en subsituant un module par un autre.

Modifions notre playbook de test, pour créer une machine virtuelle dans un vcenter VmWare. Le parfait exemple de mock :

- name: Hello Monkeyble
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
vcenter_hostname: "test"
vcenter_username: "user"
vcenter_password: "password"
esxi_hostname: "test"
tasks:
- name: "Create a virtual machine on given ESXi hostname"
community.vmware.vmware_guest:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_username }}"
password: "{{ vcenter_password }}"
folder: /DC1/vm/
name: test_vm_0001
state: present
guest_id: centos64Guest
esxi_hostname: "{{ esxi_hostname }}"
disk:
- size_gb: 10
type: thin
datastore: datastore1
hardware:
memory_mb: 512
num_cpus: 4
scsi: paravirtual
networks:
- name: VM Network
delegate_to: localhost
register: deploy_vm
- name: Debug
ansible.builtin.debug:
var: deploy_vm

Et notre fichier de test :

monkeyble_scenarios:
validate_test_1:
name: "Monkeyble hello world"
tasks_to_test:
- task: "Create a virtual machine on given ESXi hostname"
mock:
config:
monkeyble_module:
consider_changed: true
result_dict:
instance:
hw_eth0:
macaddress: "01:02:b1:03:04:9d"

On peut voir que l’on peut forcer la sortie avec result_dict pour ques les tâches suivantes du playbook puissent fonctionner.

Lançons notre test :

Terminal window
ansible-playbook -i localhost, -c local -e "@monkeyble.yml" -e "monkeyble_scenario=validate_test_1" test.yml
PLAY [Hello Monkeyble] ****************
🐵 Starting Monkeyble callback
monkeyble_scenario: validate_test_1
Monkeyble scenario: Monkeyble hello world
TASK [Create a virtual machine on given ESXi hostname] ************
🙉 Monkeyble mock module - Before: 'community.vmware.vmware_guest' Now: 'monkeyble_module'
changed: [localhost]
TASK [Debug] ****************
ok: [localhost] => {
"deploy_vm": {
"changed": true,
"failed": false,
"instance": {
"hw_eth0": {
"macaddress": "01:02:b1:03:04:9d"
}
},
"msg": "Monkeyble Mock module called. Original module: community.vmware.vmware_guest"
}
}
PLAY RECAP ************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Excellent le module a bien retourné un résultat et a pu sortir avec un état non failed et retourner le résultat défini dans le scenario monkeyble.

Conclusion

Cette collection Ansible Monkeyble vient vraiment combler un manque dans Ansible.

Je vais rapidement l’intégrer dans mes environnements de développement. Couplé à Caracdoc et Molecule et le code Ansible sera parfaitement testé. Faut-il encore que les développeurs acceptent d’écrire des tests !

Merci à Nicolas Marcq pour cette belle découverte.

Plus d’infos :