Lennart Schedin
Lennart Schedin

Reputation: 1036

Backup configuration file changes in Ansible with auto-remove of backup if file unchanged

This is my wanted flow for a playbook:

  1. Take a backup of a remote Linux configuration file I intend to (possibly) modify. The backup should be persisted to disk (on remote server) before running my main changing tasks (in case the playbook crashes half-way).

  2. Perform several Ansible tasks that may change the file. For example, 2 lineinfile and 3 blockinfile tasks.

  3. If the file has been changed by the tasks in (2.) I want to keep my backup file. Otherwise the file should be deleted.

In addition, I would like to do the same for multiple configuration files (about 10), for example /etc/ssh/sshd_config and /etc/ntp.conf etc. I would like the backup code to be as concise as possible. How can I perform the steps (1.) and (3.) in the best way?

What I have tried/investigated:

Below is a naïve and verbose example on how to do it for only 1 configuration file. For 9 more configuration files the code would get much bigger.

---
- hosts: all
  gather_facts: False
  become: yes

  tasks:
    - name: Create temporary backup of /etc/ssh/sshd_config
      copy:
        src: "/etc/ssh/sshd_config"
        remote_src: yes
        dest: "/etc/ssh/sshd_config_{{ now().strftime('%Y-%m-%d_%H_%M_%S') }}.bak"
      register: "sshd_config_backup"
      changed_when: false

    - name: Change sshd ciphers
      lineinfile:
        dest: /etc/ssh/sshd_config
        regexp: '^Ciphers '
        line: "Ciphers aes192-ctr"
      notify: "sshd config changed"

    # 3 more lineinfile/blockinfile tasks that (may) change the same file
    # name: ...
    # name: ...
    # name: ...

    # Removing backup file if not changed
    - name: Get checksum of /etc/ssh/sshd_config
      stat:
        path: "/etc/ssh/sshd_config"
        get_checksum: yes    
      register: sshd_config_stat

    - name: Remove backup of /etc/ssh/sshd_config if there are no changes
      file:
        path: "{{ sshd_config_backup.dest }}"
        state: absent
      changed_when: false
      when: sshd_config_stat.stat.checksum == sshd_config_backup.checksum

  handlers:
    - name: Reload sshd service
      listen: sshd config changed
      service:
        name: sshd
        state: reloaded

Upvotes: 0

Views: 6929

Answers (1)

Vladimir Botka
Vladimir Botka

Reputation: 67984

The play below probably does the job that you described

- name: conf_light
  hosts: all
  gather_facts: no
  become: yes

  vars_files:
    - data1.yml
  vars:
    cl_backup: yes

  tasks:
    - name: Create time-stamp
      when: cl_backup
      set_fact:
        cl_timestamp: "{{ '%Y-%m-%d_%H_%M_%S'|strftime }}"

    - name: Create backup files
      when: cl_backup
      copy:
        remote_src: yes
        src: "{{ item.value.path }}"
        dest: "{{ item.value.path }}_{{ cl_timestamp }}.bak"
      loop: "{{ cl_confs|dict2items }}"

    - name: Configure lines in files
      lineinfile:
        path: "{{ item.0.path }}"
        regexp: "{{ item.1.regexp }}"
        line: "{{ item.1.line }}"
      loop: "{{ cl_confs|subelements('lines') }}"
      notify: "{{ item.0.handler|default(omit) }}"
      register: cl_results_lines

    - name: Configure blocks in files
      blockinfile:
        path: "{{ item.0.path }}"
        marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.1.marker }}"
        block: "{{ item.1.block }}"
      loop: "{{ cl_confs|subelements('blocks') }}"
      notify: "{{ item.0.handler|default(omit) }}"
      register: cl_results_blocks

    - name: Remove backup files that did not change
      when: cl_backup
      file:
        state: absent
        path: "{{ item }}_{{ cl_timestamp }}.bak"
      loop: "{{ cl_confs|
                json_query('*.path')|
                difference(cl_results_lines.results|default([])|
                json_query('[?changed==`true`].invocation.module_args.path'))|
                difference(cl_results_blocks.results|default([])|
                json_query('[?changed==`true`].invocation.module_args.path'))
                }}"

  handlers:
    - name: ssh reload
      service:
        name: ssh
        state: reloaded

I've tested it in Ubuntu 18.04 with the data below. (It should not be a problem to fit the data to any other Linux)

shell> cat data1.yml 
cl_confs:
  sshd_config:
    path: /etc/ssh/sshd_config
    handler: ssh reload
    lines:
      - regexp: '^Ciphers '
        line: 'Ciphers aes192-ctr'
    blocks: []
  ssh_config:
    path: /etc/ssh/ssh_config
    lines: []
    blocks:
      - marker: 'srv1.example.com'
        block: |2
          Host srv1.example.com
              Protocol 2
              ForwardAgent no

It seemed like a good idea for a simple role.

Upvotes: 3

Related Questions