cifmw_helpers

That role was created to replace nested Ansible (Ansible that execute ansible or ansible-playbook binary using command/shell module) execution in this project. Role might contain tasks, required for test jobs, like molecule job.

Helper for Zuul executor cifmw general collection

The Zuul executor does not have ci-framework collection installed. It means, that when we want to drop nested Ansible execution, it would raise an errors (example):

ERROR! couldn't resolve module/action 'cifmw.general.discover_latest_image'

To avoid such error, we will be using basic Ansible behaviour which is create a symbolic link to our modules to Ansible workspace before edited playbook is executed.

Example, how to apply the workaround in Zuul CI job definition.

Before applying fix:

# .zuul.yml

- job:
    name: cifmw-adoption-base
    (...)
    roles:
      - zuul: github.com/openstack-k8s-operators/ci-framework
    pre-run:
      - ci/playbooks/multinode-customizations.yml
      - ci/playbooks/e2e-prepare.yml
      - ci/playbooks/dump_zuul_data.yml
    post-run:
      - ci/playbooks/e2e-collect-logs.yml
      - ci/playbooks/collect-logs.yml
      - ci/playbooks/multinode-autohold.yml
    (...)

After:

- job:
    name: cifmw-adoption-base
    (...)
    roles:
      - zuul: github.com/openstack-k8s-operators/ci-framework
    pre-run:
      - playbooks/cifmw_collection_zuul_executor.yml # here we added our play
      - ci/playbooks/multinode-customizations.yml
      - ci/playbooks/e2e-prepare.yml
      - ci/playbooks/dump_zuul_data.yml
    post-run:
      - ci/playbooks/e2e-collect-logs.yml
      - ci/playbooks/collect-logs.yml
      - ci/playbooks/multinode-autohold.yml
    (...)

The example playbook - playbooks/cifmw_collection_zuul_executor.yml can look like:

---
- name: Make cifmw modules to be available
  hosts: all
  tasks:
    - name: Make a symlink to local .ansible collection dir
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: symlink_cifmw_collection.yml

After doing a symbolic link of modules dir to Ansible working dir in $HOME dir, we should not have ERROR! couldn't resolve module/action error anymore.

Helper for calling nested Ansible

In many places in the project, there is nested Ansible execution done. It means, that the Ansible is running ansible or ansible-playbook inside the shell or command module. Sometimes, nested Ansible execution is done 5 times (Ansible calls Ansible calls Ansible etc.) That is later difficult to debug. More, logs are not printed directly, but they are going to special dir, where after job finish, we can read. That’s not what we should have in the CI or during local tests.

Example nested Ansible replacement

Example code, with nested Ansible execution:

- name: Run log collection
  ansible.builtin.command:
    chdir: "{{ ansible_user_dir }}/src/github.com/openstack-k8s-operators/ci-framework"
    cmd: >-
      ansible-playbook playbooks/99-logs.yml
      -e @scenarios/centos-9/base.yml

Or another example, which does not execute ansible-playbook, but ansible and directly call the role:

- name: Run run_logs tasks from cifmw_setup
  ansible.builtin.command: >
    ansible localhost
    -m include_role
    -a "name=cifmw_setup tasks_from=run_logs.yml"
    -e "@scenarios/centos-9/base.yml"
  args:
    chdir: "{{ ansible_user_dir }}/src/github.com/openstack-k8s-operators/ci-framework"

That code, can be replaced by:

- name: Read base centos-9 scenarios
  vars:
    provided_file: >
      {{ ansible_user_dir }}/src/github.com/openstack-k8s-operators/
      ci-framework/scenarios/centos-9/base.yml
  ansible.builtin.include_role:
    name: cifmw_helpers
    tasks_from: var_file.yml

- name: Run log collection
  ansible.builtin.include_role:
    name: cifmw_setup
    tasks_from: run_logs.yml
  tags:
    - logs

Read var file and set as fact

Example task execution:

- name: Read base centos-9 scenarios
  vars:
    provided_file: >
      {{ ansible_user_dir }}/src/github.com/openstack-k8s-operators/
      ci-framework/scenarios/centos-9/base.yml
  ansible.builtin.include_role:
    name: cifmw_helpers
    tasks_from: var_file.yml

Of course, before Zuul execute the playbook, it is mandatory to call playbooks/cifmw_collection_zuul_executor.yml.

Read directory and parse all files and then set as fact

For setting all files in the directory as fact, use var_dir.yml tasks. Example:

- name: Read all centos-9 scenarios dir files and set as fact
  vars:
    provided_dir: >
      {{ ansible_user_dir }}/src/github.com/openstack-k8s-operators/
      ci-framework/scenarios/centos-9/
  ansible.builtin.include_role:
    name: cifmw_helpers
    tasks_from: var_dir.yml

Set as fact various variables

In some places in our workflow, we can have a list that contains various variables like files: “@some_file.yml” or dictionaries like “some: var”. To parse them and set as a fact, use various_vars.yml task file.

- name: Example
  hosts: localhost
  tasks:
    - name: Test various vars
      vars:
        various_vars:
          - "@scenarios/centos-9/base.yml"
          - test: ok
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: various_vars.yml

    - name: Print parsed variables
      ansible.builtin.debug:
        msg: |
          "Value for file is: {{ cifmw_repo_setup_os_release }}"
          "Value for dict is: {{ test }}"

Parse inventory file and add it to inventory

Sometimes, the VMs on which action would be done are not available when the main Ansible playbook is executed. In that case, to parse the new inventory file use inventory_file.yml task, then you would be able to use delegation to execute tasks on new host.

- name: Test parsing additional inventory file
  hosts: localhost
  tasks:
    - name: Read inventory file and add it using add_host module
      vars:
        include_inventory_file: vms-inventory.yml
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: inventory_file.yml

Parse string of arguments and convert to list of variables or list of files

In some playbook, when nested Ansible is executed via shell/command module, there is a string which contains arguments to parse by the ansible-playbook binary. If nested Ansible can be removed, it would be required to parse such variables. Below example how nested Ansible execution looks like, and how it could be replaced.

NOTE: test.yaml is executed on host-1.

Example:

  • all files are on same host which execute ansible-playbook

- name: Nested Ansible execution
  hosts: localhost
  tasks:
    - name: Run ansible-playbook
      vars:
        cmd_args: "-e@somefile.yml -e @/tmp/someotherfile.yml -e myvar=test"
      ansible.builtin.command: |
        ansible-playbook "{{ cmd_args }}" test.yaml

To:

- name: Playbook that does not use nested Ansible - same host
  hosts: localhost
  vars:
    cifmw_cmd_args: "-e@somefile.yml -e @/tmp/someotherfile.yml -e myvar=test"
  tasks:
    # NOTE: The task returns fact: cifmw_cmd_args_vars and cifmw_cmd_args_files
    - name: Read inventory file and add it using add_host module
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: parse_ansible_args_string.yml

    - name: Parse only variables from cifmw_cmd_args_vars
      when: cifmw_cmd_args_vars is defined and cifmw_cmd_args_vars | length > 0
      vars:
        various_vars: "{{ cifmw_cmd_args_vars }}"
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: various_vars.yml

    - name: Read var files from cifmw_cmd_args
      when: cifmw_cmd_args_files is defined and cifmw_cmd_args_files | length > 0
      ansible.builtin.include_vars:
        file: "{{ files_item }}"
      loop: "{{ cifmw_cmd_args_files }}"
      loop_control:
        loop_var: files_item
  • files are located in remote host - controller

In alternative version, variables are available on remote host. That requires to fetch the files first to host which is executing the Ansible - include_vars reads only files that are on the host where ansible-playbook was executed. Example:

- name: Nested Ansible execution
  hosts: controller
  tasks:
    - name: Run ansible-playbook
      vars:
        cmd_args: "-e@somefile.yml -e @/tmp/someotherfile.yml -e myvar=test"
      ansible.builtin.command: |
        ansible-playbook "{{ cmd_args }}" test.yaml

To:

- name: Playbook that does not use nested Ansible - different host
  hosts: controller
  vars:
    cifmw_cmd_args: "-e@somefile.yml -e @/tmp/someotherfile.yml -e myvar=test"
  tasks:
    # NOTE: The task returns fact: cifmw_cmd_args_vars and cifmw_cmd_args_files
    - name: Read inventory file and add it using add_host module
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: parse_ansible_args_string.yml

    - name: Parse only variables from cifmw_cmd_args_vars
      when: cifmw_cmd_args_vars is defined and cifmw_cmd_args_vars | length > 0
      vars:
        various_vars: "{{ cifmw_cmd_args_vars }}"
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: various_vars.yml

    - name: Fetch cifmw_cmd_args_files to executing host
      when: cifmw_cmd_args_files is defined and cifmw_cmd_args_files | length > 0
      ansible.builtin.fetch:
        src: "{{ files_item }}"
        dest: "{{ files_item }}"
        flat: true
      loop: "{{ cifmw_cmd_args_files }}"
      loop_control:
        loop_var: files_item

    - name: Read fetched var files from cmd_args
      when: cifmw_cmd_args_files is defined and cifmw_cmd_args_files | length > 0
      ansible.builtin.include_vars:
        file: "{{ files_item }}"
      loop: "{{ cifmw_cmd_args_files }}"
      loop_control:
        loop_var: files_item

Include file

In some cases, yaml file that would have vars would be using Jinja2 vars, which means that on setting fact, variable would not be “translated”. It means, that if variable is:

test: "{{ ansible_user_dir }}"

Result when we will use var_file.yml, would be:

{ "test": "{{ ansible_user_dir}}" }

This is not want we would like to have. The ansible_user_dir should be “translated”, so expected value should be:

{ "test": "/home/testuser" }

This helper would include vars properly.

Example:

- name: Test include vars
  hosts: somehost
  tasks:
    - name: Read group_vars all file
      vars:
        included_file: group_vars/all.yml
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: include_file.yml

    - name: Print vars from group_vars all
      ansible.builtin.debug:
        msg: |
          {{ noop_helper_var }}

Similar to what include_file is doing, but instead of parsing single file, it parse all yaml files available in the directory.

Include dir

- name: Test include vars - dr
  hosts: somehost
  tasks:
    - name: Read group_vars dir file
      vars:
        included_dir: ./group_vars
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: include_dir.yml

    - name: Print vars from group_vars all
      ansible.builtin.debug:
        msg: |
          {{ noop_helper_var }}

Test project helpers

Start CRC

In some CI jobs, we are using crc to verify the role functionality. Example usage:

  • Setup crc with default resources

    - name: Start CRC
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: crc_start.yml
  • Setup crc with limited resources, like memory, disk, cpu - more

    - name: Start CRC
      vars:
        cifmw_helpers_crc_additional_params: "--memory 14000 --disk-size 80 --cpus 6"
      ansible.builtin.include_role:
        name: cifmw_helpers
        tasks_from: crc_start.yml