0

I would like to define rescue or rollback executions within my Ansible structure. I am using this recommended structure:

roles
  rolename
    files
    tasks
    templates
    handlers
    ...

I know that I can set blocks and within them a rescue section. But this seems a little clumsy to me and I would like to define all the rollback/rescue options within its own folder like

roles
  rolename
    ...
    tasks
    rollback/rescue
    handlers
    ...

This would make much more sense to me. I am speaking here about rollback, because I would like to be able to rollback a setup that I have created once with this role. I understand that the concept of rescue is a little bit different because it aims to fire when there is an error approached in executing a role. But I am hoping that I could use the concepts together while defining an anti-step to each step that I have defined in the role. And these anti-steps should have their own place and not included in the tasks files.

2
  • 1
    What prevents you from "defining an anti-step to each step" in rollback and including them whenever you want? Commented Mar 7, 2023 at 23:00
  • Maybe an unpopular opinion, but your rollback should be to a previous state. This means versioning your roles and applying the desired version of the role to the target system, not implementing "anti-tasks" in the role. Commented Mar 8, 2023 at 6:58

1 Answer 1

1

For example, enable backup and register the results. As a last task create a record in the journal (see the declaration of the variables in the details under the line) and keep it in the cache

shell> cat roles/test_rollback/tasks/config.yml 
- name: config_01
  template:
    src: test.ini.j2
    dest: /tmp/test.ini
    backup: true
  register: config_01

- name: Write journal
  set_fact:
    journal: "{{ journal|d({})|combine({timestamp: journal_config_dict}) }}"
    cacheable: true
  vars:
    timestamp: "{{ '%Y-%m-%d_%H_%M_%S'|strftime }}"

If you want to rollback the changes set the rollback_journal_record

shell> cat roles/test_rollback/rollback/config.yml 
- copy:
    remote_src: true
    src: "{{ item.value.backup_file }}"
    dest: "{{ item.value.dest }}"
  loop: "{{ journal[rollback_journal_record]|dict2items|selectattr('value.changed') }}"
  loop_control:
    label: "{{ item.key }}"

Let's test it. Given the file on the remote host

shell> ssh admin@test_11 cat /tmp/test.ini
test = X

Run the playbook

shell> ansible-playbook pb.yml
  ...
TASK [test_rollback : config_01] *************************************************************
changed: [test_11]

TASK [test_rollback : Write journal] *********************************************************
ok: [test_11]
  ...

The file changed

shell> ssh admin@test_11 cat /tmp/test.ini
test = A

and the record was added to the journal

shell> cat /tmp/ansible_cache.json/ansible_facts_test_11 
{
    "journal": {
        "2023-03-08_02_33_48": {
            "config_01": {
                "backup_file": "/tmp/test.ini.50045.2023-03-08@01:33:48~",
                "changed": true,
                "checksum": "6676ed8ea4caa804d3c5f0247c03ece8b7a80b0b",
                "dest": "/tmp/test.ini",
                "diff": [],
                "failed": false,
                "gid": 0,
                "group": "wheel",
                "md5sum": "0740a0962b43fa7bedf3dcd97e96697f",
                "mode": "0644",
                "owner": "root",
                "size": 9,
                "src": "/home/admin/.ansible/tmp/ansible-tmp-1678239225.046748-1753646-178984183659153/source",
                "state": "file",
                "uid": 0
            }
        }
    }
}

See the records in the journal

shell>ansible-playbook pb.yml -t info -e debug_journal_records=true
  ...
TASK [test_rollback : debug] *****************************************************************
ok: [test_11] => 
  msg: |-
    Journal records:
    - 2023-03-08_02_33_48
  ...

See the changed file(s) in the record

shell> ansible-playbook pb.yml -t info -e debug_journal_record=2023-03-08_02_33_48
  ...
TASK [test_rollback : debug] *****************************************************************
ok: [test_11] => 
  msg: |-
    Journal changes of 2023-03-08_02_33_48:
    config_01:
        backup_file: /tmp/test.ini.50045.2023-03-08@01:33:48~
        changed: true
        checksum: 6676ed8ea4caa804d3c5f0247c03ece8b7a80b0b
        dest: /tmp/test.ini
        diff: []
        failed: false
        gid: 0
        group: wheel
        md5sum: 0740a0962b43fa7bedf3dcd97e96697f
        mode: '0644'
        owner: root
        size: 9
        src: /home/admin/.ansible/tmp/ansible-tmp-1678239225.046748-1753646-178984183659153/source
        state: file
        uid: 0
  ...

Rollback the changed file(s)

shell> ansible-playbook pb.yml -t rollback -e rollback_journal_record=2023-03-08_02_33_48
  ...
TASK [test_rollback : copy] ******************************************************************
changed: [test_11] => (item=config_01)
  ...

The file was restored from the backup

shell> ssh admin@test_11 cat /tmp/test.ini
test = X

Example of a complete project for testing

shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
    └── test_rollback
        ├── defaults
        │   └── main.yml
        ├── rollback
        │   ├── config.yml
        │   └── main.yml
        ├── tasks
        │   ├── config.yml
        │   ├── info.yml
        │   ├── main.yml
        │   └── rollback.yml
        └── templates
            └── test.ini.j2

6 directories, 11 files

Enable caching

shell> cat ansible.cfg 
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
ansible_managed = {file}
deprecation_warnings=true
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
# fact_caching
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache.json
fact_caching_prefix = ansible_facts_
fact_caching_timeout = 86400
shell> cat hosts 
test_11
test_12
test_13
shell> cat pb.yml 
- hosts: test_11

  roles:
    - test_rollback

The role

shell> cat roles/test_rollback/tasks/main.yml 
- import_tasks: info.yml
  tags: info

- import_tasks: rollback.yml
  when: rollback_journal_record is defined
  tags: rollback

- import_tasks: config.yml
  tags: config
shell> cat roles/test_rollback/tasks/info.yml 
- debug:
    msg: |
      Journal records:
      {{ journal.keys()|list|to_nice_yaml }}
  when: debug_journal_records|d(false)|bool

- debug:
    msg: |
      Journal changes of {{ debug_journal_record }}:
      {{ journal[debug_journal_record]|dict2items|
         selectattr('value.changed')|items2dict|to_nice_yaml }}
  when: debug_journal_record is defined
shell> cat roles/test_rollback/tasks/rollback.yml 
- import_tasks: rollback/main.yml
  
- meta: end_play
shell> cat roles/test_rollback/tasks/config.yml 
- name: config_01
  template:
    src: test.ini.j2
    dest: /tmp/test.ini
    backup: true
  register: config_01

- name: Write journal
  set_fact:
    journal: "{{ journal|d({})|combine({timestamp: journal_config_dict}) }}"
    cacheable: true
  vars:
    timestamp: "{{ '%Y-%m-%d_%H_%M_%S'|strftime }}"
shell> cat roles/test_rollback/rollback/main.yml 
- debug:
    msg: |
      Rollback {{ rollback_journal_record }}:
      {{ journal[rollback_journal_record]|dict2items|
         selectattr('value.changed')|
         map(attribute='value.dest')|to_nice_yaml }}
  when: debug|d(false)|bool

- import_tasks: rollback/config.yml
shell> cat roles/test_rollback/rollback/config.yml 
- copy:
    remote_src: true
    src: "{{ item.value.backup_file }}"
    dest: "{{ item.value.dest }}"
  loop: "{{ journal[rollback_journal_record]|dict2items|selectattr('value.changed') }}"
  loop_control:
    label: "{{ item.key }}"
shell> cat roles/test_rollback/defaults/main.yml 
# Journal
journal_config_varnames: "{{ q('varnames', '^config_*') }}"
journal_config_vars: "{{ q('vars', *q('varnames', '^config_*')) }}"
journal_config_dict: "{{ dict(journal_config_varnames|zip(journal_config_vars)) }}"
shell> cat roles/test_rollback/templates/test.ini.j2 
test = A

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.