Aller au contenu principal

Monkeyble un callback Ansible de tests unitaires

· 7 minutes de lecture
Stéphane ROBERT
Consultant DevOps

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 :

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 :

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 :

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 :

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 :

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 :